From 54d774e4ed82901402cab8e6d279df83dcc300d1 Mon Sep 17 00:00:00 2001 From: Myrlhyn <161041172+Myrlhyn@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:06:21 +0100 Subject: [PATCH 01/26] Create Localizable.strings Translation to french - step 1 --- fr.lproj/Localizable.strings | 316 +++++++++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 fr.lproj/Localizable.strings diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings new file mode 100644 index 00000000..4556678e --- /dev/null +++ b/fr.lproj/Localizable.strings @@ -0,0 +1,316 @@ +/* + Localizable.strings + Meshtastic + + Copyright(c) Garth Vander Houwen on 12/12/22. + +*/ +"about"="À propos"; +"about.meshtastic"="À propos de Meshtastic"; +"admin"="Administrateur"; +"admin.log"="Journal des messages administrateur"; +"ago"="auparavant"; +"airtime"="Temps d'émission"; +"always.on"="En permanence"; +"ambient.lighting"="Lumière ambiante"; +"ambient.lighting.config"="Configuration de la lumière ambiante"; +"appsettings"="Réglages de l'application"; +"appsettings.provide.location"="Partager la position"; +"appsettings.smartposition"="Position intelligente"; +"are.you.sure"="Êtes-vous sûr ?"; +"ascii.capable"="ASCII Compatible"; +"available.radios"="Radios disponibles"; +"automatic.detection"="Détection automatique"; +"battery.level"="Niveau de batterie"; +"ble.name"="Nom du BLE"; +"ble.connection.timeout %d %@"="Connexion impossible après %d essais avec %@. Allez dans Réglages > Bluetooth et essayez de faire de faire > Oublier cet appareil."; +"ble.errorcode.6 %@"="%@ L'application se reconnectera automatiquement à la radio en favori dès qu'elle sera à nouveau visibile."; +"ble.errorcode.14 %@"="%@ Cette erreur ne peut généralement pas être corrigée sans aller dans Réglages > Bluetooth et faire > Oublier cet appareil, puis de reconnecter la radio."; +"ble.errorcode.pin %@"="%@ Merci d'essayer à nouveau en vérifiant bien le code PIN."; +"bluetooth"="Bluetooth"; +"bluetooth.off"="Le Bluetooth is arrêté"; +"bluetooth.config"="Configuration du Bluetooth"; +"bluetooth.mode.randompin"="Code PIN aléatoire"; +"bluetooth.mode.fixedpin"="Code PIN fixe"; +"bluetooth.mode.nopin"="Sans copde PIN (connexion directe)"; +"bluetooth.pairingmode"="Mode d'appairage"; +"bluetooth.pin.validation"="Le code pin BLE doit faire 6 chiffres."; +"bytes"="Octets"; +"cancel"="Annuler"; +"canned.messages"="Messages Canned"; +"canned.messages.config"="Configuration des messages Canned"; +"canned.messages.preset.manual"="Configuration manuelle"; +"canned.messages.preset.rakrotary"="Module d'encodage rotatif RAK"; +"canned.messages.preset.cardkb"="Clavier M5 Stack Card KB / RAK"; +"channel"="Canal"; +"channel.role.disabled"="Désactivé"; +"channel.role.primary"="Principal"; +"channel.role.secondary"="Secondaire"; +"channel.utilization"="Utilisation du canal"; +"channels"="Canaux"; +"clear.app.data"="Effacer les données de l'application"; +"clear.log"="Effacer"; +"close"="Fermer"; +"config.save.confirm"="After config values save the node will reboot."; +"communicating"="Communicating with device. ."; +"connected.radio"="Connected Radio"; +"connected"="Bluetooth Connected"; +"connecting"="Connecting . ."; +"contacts"="Contacts"; +"contacts %@"="Contacts (%@)"; +"copy"="Copy"; +"current"="Current"; +"default"="Default"; +"delete"="Delete"; +"detection.sensor"="Detection Sensor"; +"detection.sensor.config"="Detection Sensor Config"; +"detection.sensor.log"="Detection Sensor Log"; +"device"="Device"; +"device.config"="Device Config"; +"device.metrics.delete"="Delete all device metrics?"; +"device.metrics.log"="Device Metrics Log"; +"device.role.client"="App connected or stand alone messaging device."; +"device.role.clientmute"="Device that does not forward packets from other devices."; +"device.role.clienthidden"="Device that only broadcasts as needed for stealth or power savings."; +"device.role.tracker"="Broadcasts GPS position packets as priority."; +"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery."; +"device.role.sensor"="Broadcasts telemetry packets as priority."; +"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; +"device.role.repeater"="Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list."; +"device.role.router"="Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list."; +"device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile devices."; +"direct.messages"="Direct Messages"; +"dismiss.keyboard"="Dismiss"; +"display"="Display"; +"display.config"="Display Config"; +"distance"="Distance"; +"disconnect"="Disconnect"; +"echo"="Echo"; +"email.address"="Email Address"; +"enabled"="Enabled"; +"encrypted"="Encrypted"; +"external.notification"="External Notification"; +"external.notification.config"="External Notification Config"; +"finish"="Finish"; +"firmware.version"="Firmware Version"; +"firmware.version.unsupported"="Unsupported Firmware Version Detected, unable to connect to device."; +"gas"="Gas"; +"gas.resistance"="Gas Resistance"; +"generate.qr.code"="Generate QR Code"; +"gpsformat.dec"="Decimal Degrees Format"; +"gpsformat.dms"="Degrees Minutes Seconds"; +"gpsformat.utm"="Universal Transverse Mercator"; +"gpsformat.mgrs"="Military Grid Reference System"; +"gpsformat.olc"="Open Location Code (aka Plus Codes)"; +"gpsformat.osgr"="Ordnance Survey Grid Reference"; +"gpsmode.disabled"="Disabled"; +"gpsmode.enabled"="Enabled"; +"gpsmode.notPresent"="Not Present"; +"heard"="Heard"; +"heard.last"="Last Heard"; +"hybrid"="Hybrid"; +"hybrid.flyover"="Hybrid Flyover"; +"include"="Include"; +"inputevent.none"="None"; +"inputevent.up"="Up"; +"inputevent.down"="Down"; +"inputevent.left"="Left"; +"inputevent.right"="Right"; +"inputevent.select"="Select"; +"inputevent.back"="Back"; +"inputevent.cancel"="Cancel"; +"interval.one.second"="One Second"; +"interval.two.seconds"="Two Seconds"; +"interval.three.seconds"="Three Seconds"; +"interval.four.seconds"="Four Seconds"; +"interval.five.seconds"="Five Seconds"; +"interval.ten.seconds"="Ten Seconds"; +"interval.fifteen.seconds"="Fifteen Seconds"; +"interval.twenty.seconds"="Twenty Seconds"; +"interval.twentyfive.seconds"="Twenty Five Seconds"; +"interval.thirty.seconds"="Thirty Seconds"; +"interval.fortyfive.seconds"="Forty Five Seconds"; +"interval.one.minute"="One Minute"; +"interval.two.minutes"="Two Minutes"; +"interval.five.minutes"="Five Minutes"; +"interval.ten.minutes"="Ten Minutes"; +"interval.fifteen.minutes"="Fifteen Minutes"; +"interval.thirty.minutes"="Thirty Minutes"; +"interval.one.hour"="One Hour"; +"interval.two.hours"="Two Hours"; +"interval.three.hours"="Three Hours"; +"interval.four.hours"="Four Hours"; +"interval.five.hours"="Five Hours"; +"interval.six.hours"="Six Hours"; +"interval.twelve.hours"="Twelve Hours"; +"interval.eighteen.hours"="Eighteen Hours"; +"interval.twentyfour.hours"="Twenty Four Hours"; +"interval.thirtysix.hours"="Thirty Six Hours"; +"interval.fortyeight.hours"="Forty Eight Hours"; +"interval.seventytwo.hours"="Seventy Two Hours"; +"keyboard.type"="Keyboard Type"; +"logging"="Logging"; +"lora"="LoRa"; +"lora.config"="LoRa Config"; +"map"="Mesh Map"; +"map.type"="Default Type"; +"map.centering"="Centering Mode"; +"map.tiles.delete"="Delete All Map Tiles"; +"map.recentering"="Automatic Re-centering"; +"map.use.legacy"="Use Legacy Mesh Map"; +"map.usertrackingmode"="User tracking mode"; +"map.usertrackingmode.follow"="Follow"; +"map.usertrackingmode.followwithheading"="Follow with heading"; +"map.usertrackingmode.none"="None"; +"mesh.live.activity"="Mesh Live Activity"; +"mesh.log"="Mesh Log"; +"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; +"mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; +"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; +"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@"; +"mesh.log.cannedmessages.messages.received %@"="Canned Messages Messages Received For: %@"; +"mesh.log.channel.sent %@ %d"="Sent a Channel for: %@ Channel Index %d"; +"mesh.log.channel.received %d %@"="Channel %d received from: %@"; +"mesh.log.device.config %@"="Device config received: %@"; +"mesh.log.display.config %@"="Display config received: %@"; +"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@"; +"mesh.log.device.metadata.received %@"="Device Metadata received from: %@"; +"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@"; +"mesh.log.externalnotification.config %@"="External Notification module config received: %@"; +"mesh.log.lora.config %@"="LoRa config received: %@"; +"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@"; +"mesh.log.mqtt.config %@"="MQTT module config received: %@"; +"mesh.log.myinfo %@"="MyInfo received: %@"; +"mesh.log.network.config %@"="Network config received: %@"; +"mesh.log.nodeinfo.received %@"="Node info received for: %@"; +"mesh.log.position.config %@"="Positon config received: %@"; +"mesh.log.position.received %@"="Position Packet received from node: %@"; +"mesh.log.rangetest.config %@"="Range Test module config received: %@"; +"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; +"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; +"mesh.log.serial.config %@"="Serial module config received: %@"; +"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; +"mesh.log.storeforward.config %@"="Store & Forward module config received: %@"; +"mesh.log.telemetry.config %@"="Telemetry module config received: %@"; +"mesh.log.telemetry.received %@"="Telemetry received for: %@"; +"mesh.log.textmessage.received"="Message received from the text message app."; +"mesh.log.textmessage.send.failed %@"="Message Send Failed, not properly connected to %@"; +"mesh.log.textmessage.sent %@ %@ %@"="Sent message %@ from %@ to %@"; +"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly."; +"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@"; +"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; +"mesh.log.wantconfig %@"="Issuing Want Config to %@"; +"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; +"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; +"message"="Message"; +"message.details"="Message Details"; +"messages"="Messages"; +"mode"="Mode"; +"module.configuration"="Module Configuration"; +"mqtt"="MQTT"; +"mqtt.connect"="Connect to MQTT"; +"mqtt.config"="MQTT Config"; +"mqtt.clientproxy"="MQTT Client Proxy"; +"mqtt.disconnect"="Disconnect from MQTT"; +"mqtt.username"="Username"; +"name"="Name"; +"network"="Network"; +"network.config"="Network Config"; +"nodes"="Nodes"; +"nodes %@"="Nodes (%@)"; +"no.nodes"="No Meshtastic Nodes Found"; +"not.connected"="No device connected"; +"numbers.punctuation"="Numbers and Punctuation"; +"off"="Off"; +"offline"="Offline"; +"on.boot"="On Boot Only"; +"options"="Options"; +"password"="Password"; +"pause"="Pause"; +"phone.gps"="Phone GPS"; +"phone.gps.interval.description"="How frequently your phone will send your location to the device, location updates to the mesh are managed by the device."; +"position"="Position"; +"position.config"="Position Config"; +"preferred.radio"="Preferred Radio"; +"radio.configuration"="Radio Configuration"; +"range.test"="Range Test"; +"range.test.blocked"="Block Range Test"; +"range.test.config"="Range Test Config"; +"reply"="Reply"; +"reboot"="Reboot"; +"reboot.node"="Reboot node?"; +"received.ack"="Received Ack"; +"received.ack.real"="Recipient Ack"; +"resume"="Resume"; +"ringtone"="Ringtone"; +"ringtone.config"="Ringtone Config"; +"route.recorder"="Route Recorder"; +"routes"="Routes"; +"routing.acknowledged"="Acknowledged"; +"routing.noroute"="No Route"; +"routing.gotnak"="Received a negative acknowledgment"; +"routing.timeout"="Timeout"; +"routing.nointerface"="No Interface"; +"routing.maxretransmit"="Max Retransmission Reached"; +"routing.nochannel"="No Channel"; +"routing.toolarge"="The packet is too large"; +"routing.noresponse"="No Response"; +"routing.dutycyclelimit"="Regional Duty Cycle Limit Reached"; +"routing.badRequest"="Bad Request"; +"routing.notauthorized"="Not Authorized"; +"satellite"="Satellite"; +"satellite.flyover"="Satellite Flyover"; +"save"="Save"; +"save.config %@"="Save Config for %@"; +"serial"="Serial"; +"serial.config"="Serial Config"; +"serial.mode.default"="Default"; +"serial.mode.simple"="Simple"; +"serial.mode.proto"="Protobufs"; +"serial.mode.txtmsg"="Text Message"; +"serial.mode.nmea"="NMEA Positions"; +"settings"="Settings"; +"share.channels"="Share QR Code"; +"share.position"="Share Position"; +"subscribed"="Subscribed to mesh"; +"select.contact"="Select a Contact"; +"select.node"="Select a Node"; +"select.menu.item"="Select an item from the menu"; +"set.region"="Set LoRa Region"; +"standard"="Standard"; +"standard.muted"="Standard Muted"; +"start"="Start"; +"storeforward"="Store & Forward"; +"storeforward.config"="Store & Forward Config"; +"storeforward.heartbeat"="Send Heartbeat"; +"ssid"="SSID"; +"tapback"="Tapback Response"; +"tapback.heart"="Heart"; +"tapback.thumbsup"="Thumbs Up"; +"tapback.thumbsdown"="Thumbs Down"; +"tapback.haha"="HaHa"; +"tapback.exclamation"="Exclamation Mark"; +"tapback.question"="Question Mark"; +"tapback.poop"="Poop"; +"telemetry"="Telemetry (Sensors)"; +"telemetry.config"="Telemetry Config"; +"timeout"="Timeout"; +"timestamp"="Timestamp"; +"tip.bluetooth.connect.title"="Connected Radio"; +"tip.bluetooth.connect.message"="Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; +"tip.channels.create.title"="Manage Channels"; +"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)"; +"tip.channels.share.title"="Sharing Meshtastic Channels"; +"tip.channels.share.message"="A Meshtastic QR code contains the LoRa config and channel values needed to communicate. Most mesh activity takes place on the required Primary channel. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. Other channels are for private groups, each with its own key."; +"tip.messages.title"="Messages"; +"tip.messages.message"="You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details."; +"twitter"="Twitter"; +"unknown"="Unknown"; +"unknown.age"="Unknown Age"; +"unset"="Unset"; +"update.firmware"="Update Your Firmware"; +"update.interval"="Update Interval"; +"user"="User"; +"user.details"="User Details"; +"voltage"="Voltage"; +"waiting"="Waiting. . ."; From 6a92435323665897e0fe414c71f4746448917728 Mon Sep 17 00:00:00 2001 From: Myrlhyn <161041172+Myrlhyn@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:20:24 +0100 Subject: [PATCH 02/26] Update Localizable.strings Translation to french, step 2 --- fr.lproj/Localizable.strings | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 4556678e..a4703f3d 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -51,22 +51,22 @@ "clear.app.data"="Effacer les données de l'application"; "clear.log"="Effacer"; "close"="Fermer"; -"config.save.confirm"="After config values save the node will reboot."; -"communicating"="Communicating with device. ."; -"connected.radio"="Connected Radio"; -"connected"="Bluetooth Connected"; -"connecting"="Connecting . ."; +"config.save.confirm"="Une fois la configuration sauvegardée, le noeud redémarrera."; +"communicating"="Communication avec l'appareil en cours. ."; +"connected.radio"="Radio connectée"; +"connected"="Bluetooth connecté"; +"connecting"="Connexion . ."; "contacts"="Contacts"; "contacts %@"="Contacts (%@)"; -"copy"="Copy"; -"current"="Current"; -"default"="Default"; -"delete"="Delete"; -"detection.sensor"="Detection Sensor"; -"detection.sensor.config"="Detection Sensor Config"; -"detection.sensor.log"="Detection Sensor Log"; -"device"="Device"; -"device.config"="Device Config"; +"copy"="Copier"; +"current"="Actuel"; +"default"="Par défaut"; +"delete"="Effacer"; +"detection.sensor"="Capteur de détection"; +"detection.sensor.config"="Configuration du capteur de détection"; +"detection.sensor.log"="Journal du capteur de détection"; +"device"="Appareil"; +"device.config"="Configuration de l'appareil"; "device.metrics.delete"="Delete all device metrics?"; "device.metrics.log"="Device Metrics Log"; "device.role.client"="App connected or stand alone messaging device."; From 982fd4e1f7e09e409c077b140b032763c1ebfe5f Mon Sep 17 00:00:00 2001 From: Myrlhyn <161041172+Myrlhyn@users.noreply.github.com> Date: Sun, 25 Feb 2024 17:56:10 +0100 Subject: [PATCH 03/26] Update Localizable.strings --- fr.lproj/Localizable.strings | 50 ++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index a4703f3d..7b7c954d 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -67,33 +67,33 @@ "detection.sensor.log"="Journal du capteur de détection"; "device"="Appareil"; "device.config"="Configuration de l'appareil"; -"device.metrics.delete"="Delete all device metrics?"; -"device.metrics.log"="Device Metrics Log"; -"device.role.client"="App connected or stand alone messaging device."; -"device.role.clientmute"="Device that does not forward packets from other devices."; -"device.role.clienthidden"="Device that only broadcasts as needed for stealth or power savings."; -"device.role.tracker"="Broadcasts GPS position packets as priority."; -"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery."; -"device.role.sensor"="Broadcasts telemetry packets as priority."; -"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; -"device.role.repeater"="Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list."; -"device.role.router"="Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list."; -"device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile devices."; -"direct.messages"="Direct Messages"; -"dismiss.keyboard"="Dismiss"; -"display"="Display"; -"display.config"="Display Config"; +"device.metrics.delete"="Effacer toutes les mesures de l’appareil?"; +"device.metrics.log"="Journal des mesures de l'appareil"; +"device.role.client"="Apploication connectée ou appareil de messagerie indépendant."; +"device.role.clientmute"="Appareil ne transmettant pas les paquets provenant d'autres appareils."; +"device.role.clienthidden"="Appareil ne diffusant que si nécessaire pour la discrétion et l'économie d'énergie."; +"device.role.tracker"="Transmet les paquets de positions GPS en priorité."; +"device.role.lostandfound"="Transmet régulièrement la position par message dans le canal par défaut pour vous aider à retrouver l'appareil."; +"device.role.sensor"="Transmet les paquets de télémétrie en priorité."; +"device.role.tak"="Optimisé pour le système de communication ATAK, diminue les émissions de routine."; +"device.role.repeater"="Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages avec un minimum de surcharge. Invisible dans la liste des noeuds."; +"device.role.router"="Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages. Visible dans la liste des noeuds."; +"device.role.routerclient"="Combinaison des modes ROUTER et CLIENT. Pas pour les appareils mobiles."; +"direct.messages"="Messages directs"; +"dismiss.keyboard"="Annuler"; +"display"="Écran"; +"display.config"="Configuration de l'écran"; "distance"="Distance"; -"disconnect"="Disconnect"; +"disconnect"="Déconnecter"; "echo"="Echo"; -"email.address"="Email Address"; -"enabled"="Enabled"; -"encrypted"="Encrypted"; -"external.notification"="External Notification"; -"external.notification.config"="External Notification Config"; -"finish"="Finish"; -"firmware.version"="Firmware Version"; -"firmware.version.unsupported"="Unsupported Firmware Version Detected, unable to connect to device."; +"email.address"="Adresse mail"; +"enabled"="Activé(e)"; +"encrypted"="Encrypté(e)"; +"external.notification"="Notification extérieure"; +"external.notification.config"="Configuration de la notification extérieure"; +"finish"="Terminer"; +"firmware.version"="Version du firmware"; +"firmware.version.unsupported"="Version non supportée du firmware détectée, impossible de se connecter à l'appareil."; "gas"="Gas"; "gas.resistance"="Gas Resistance"; "generate.qr.code"="Generate QR Code"; From 346eeb54c357b9c1a0f2410c6ead17d14fcba2d1 Mon Sep 17 00:00:00 2001 From: Myrlhyn <161041172+Myrlhyn@users.noreply.github.com> Date: Sun, 25 Feb 2024 20:17:52 +0100 Subject: [PATCH 04/26] Update Localizable.strings --- fr.lproj/Localizable.strings | 140 +++++++++++++++++------------------ 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 7b7c954d..38f2a646 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -85,85 +85,85 @@ "display.config"="Configuration de l'écran"; "distance"="Distance"; "disconnect"="Déconnecter"; -"echo"="Echo"; +"echo"="Écho"; "email.address"="Adresse mail"; -"enabled"="Activé(e)"; -"encrypted"="Encrypté(e)"; +"enabled"="Activé"; +"encrypted"="Encrypté"; "external.notification"="Notification extérieure"; "external.notification.config"="Configuration de la notification extérieure"; "finish"="Terminer"; "firmware.version"="Version du firmware"; "firmware.version.unsupported"="Version non supportée du firmware détectée, impossible de se connecter à l'appareil."; -"gas"="Gas"; -"gas.resistance"="Gas Resistance"; -"generate.qr.code"="Generate QR Code"; -"gpsformat.dec"="Decimal Degrees Format"; -"gpsformat.dms"="Degrees Minutes Seconds"; -"gpsformat.utm"="Universal Transverse Mercator"; +"gas"="Gaz"; +"gas.resistance"="Résistence du gaz"; +"generate.qr.code"="Générer un QR Code"; +"gpsformat.dec"="Format décimal pour les degrés"; +"gpsformat.dms"="Degrés Minutes Secondes"; +"gpsformat.utm"="Projection Mercator Transverse Universelle"; "gpsformat.mgrs"="Military Grid Reference System"; -"gpsformat.olc"="Open Location Code (aka Plus Codes)"; +"gpsformat.olc"="Open Location Code (alias Plus Codes)"; "gpsformat.osgr"="Ordnance Survey Grid Reference"; -"gpsmode.disabled"="Disabled"; -"gpsmode.enabled"="Enabled"; -"gpsmode.notPresent"="Not Present"; -"heard"="Heard"; -"heard.last"="Last Heard"; -"hybrid"="Hybrid"; -"hybrid.flyover"="Hybrid Flyover"; -"include"="Include"; -"inputevent.none"="None"; -"inputevent.up"="Up"; -"inputevent.down"="Down"; -"inputevent.left"="Left"; -"inputevent.right"="Right"; -"inputevent.select"="Select"; -"inputevent.back"="Back"; -"inputevent.cancel"="Cancel"; -"interval.one.second"="One Second"; -"interval.two.seconds"="Two Seconds"; -"interval.three.seconds"="Three Seconds"; -"interval.four.seconds"="Four Seconds"; -"interval.five.seconds"="Five Seconds"; -"interval.ten.seconds"="Ten Seconds"; -"interval.fifteen.seconds"="Fifteen Seconds"; -"interval.twenty.seconds"="Twenty Seconds"; -"interval.twentyfive.seconds"="Twenty Five Seconds"; -"interval.thirty.seconds"="Thirty Seconds"; -"interval.fortyfive.seconds"="Forty Five Seconds"; -"interval.one.minute"="One Minute"; -"interval.two.minutes"="Two Minutes"; -"interval.five.minutes"="Five Minutes"; -"interval.ten.minutes"="Ten Minutes"; -"interval.fifteen.minutes"="Fifteen Minutes"; -"interval.thirty.minutes"="Thirty Minutes"; -"interval.one.hour"="One Hour"; -"interval.two.hours"="Two Hours"; -"interval.three.hours"="Three Hours"; -"interval.four.hours"="Four Hours"; -"interval.five.hours"="Five Hours"; -"interval.six.hours"="Six Hours"; -"interval.twelve.hours"="Twelve Hours"; -"interval.eighteen.hours"="Eighteen Hours"; -"interval.twentyfour.hours"="Twenty Four Hours"; -"interval.thirtysix.hours"="Thirty Six Hours"; -"interval.fortyeight.hours"="Forty Eight Hours"; -"interval.seventytwo.hours"="Seventy Two Hours"; -"keyboard.type"="Keyboard Type"; -"logging"="Logging"; +"gpsmode.disabled"="Désactivé"; +"gpsmode.enabled"="Activé"; +"gpsmode.notPresent"="Absent"; +"heard"="Entendre"; +"heard.last"="Entendu pour la dernière fois"; +"hybrid"="Hybride"; +"hybrid.flyover"="Survol hybride"; +"include"="Inclure"; +"inputevent.none"="Aucun"; +"inputevent.up"="Haut"; +"inputevent.down"="Bas"; +"inputevent.left"="Gauche"; +"inputevent.right"="Droite"; +"inputevent.select"="Sélectionner"; +"inputevent.back"="Retour"; +"inputevent.cancel"="Annuler"; +"interval.one.second"="Une seconde"; +"interval.two.seconds"="Deux secondes"; +"interval.three.seconds"="Trois secondes"; +"interval.four.seconds"="Quatre secondes"; +"interval.five.seconds"="Cinq secondes"; +"interval.ten.seconds"="Dix secondes"; +"interval.fifteen.seconds"="Quinze secondes"; +"interval.twenty.seconds"="Vingr secondes"; +"interval.twentyfive.seconds"="Vignt cinq secondes"; +"interval.thirty.seconds"="Trente secondes"; +"interval.fortyfive.seconds"="Quanrante cinq secondes"; +"interval.one.minute"="Une minute"; +"interval.two.minutes"="Deux minutes"; +"interval.five.minutes"="Cinq minutes"; +"interval.ten.minutes"="Dix minutes"; +"interval.fifteen.minutes"="Quinze minutes"; +"interval.thirty.minutes"="Trente minutes"; +"interval.one.hour"="Une heure"; +"interval.two.hours"="Deux heures"; +"interval.three.hours"="Trois heures"; +"interval.four.hours"="Quatre heures"; +"interval.five.hours"="Cinq heures"; +"interval.six.hours"="Six heures"; +"interval.twelve.hours"="Douze heures"; +"interval.eighteen.hours"="Dix huit heures"; +"interval.twentyfour.hours"="Vingt quatre heures"; +"interval.thirtysix.hours"="Trente six heures"; +"interval.fortyeight.hours"="Quarante huit heures"; +"interval.seventytwo.hours"="Soixante douze heures"; +"keyboard.type"="Type de clavier"; +"logging"="Enregistrement"; "lora"="LoRa"; -"lora.config"="LoRa Config"; -"map"="Mesh Map"; -"map.type"="Default Type"; -"map.centering"="Centering Mode"; -"map.tiles.delete"="Delete All Map Tiles"; -"map.recentering"="Automatic Re-centering"; -"map.use.legacy"="Use Legacy Mesh Map"; -"map.usertrackingmode"="User tracking mode"; -"map.usertrackingmode.follow"="Follow"; -"map.usertrackingmode.followwithheading"="Follow with heading"; -"map.usertrackingmode.none"="None"; -"mesh.live.activity"="Mesh Live Activity"; -"mesh.log"="Mesh Log"; +"lora.config"="Configuration LoRa"; +"map"="Carte de maillage"; +"map.type"="Type par défaut"; +"map.centering"="Mode centré"; +"map.tiles.delete"="Supprimer toutes les tuiles de carte"; +"map.recentering"="Recentrage automatique"; +"map.use.legacy"="Utiliser l'ancienne génération de carte de maillage"; +"map.usertrackingmode"="Mode suivre l'utilisateur"; +"map.usertrackingmode.follow"="Suivre"; +"map.usertrackingmode.followwithheading"="Suivre avec le cap"; +"map.usertrackingmode.none"="Aucun"; +"mesh.live.activity"="Activité en direct du maillage"; +"mesh.log"="Journal du maillage"; "mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; "mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; From 6c927d36a0923d3a79e16d519a44a51359e99060 Mon Sep 17 00:00:00 2001 From: Myrlhyn <161041172+Myrlhyn@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:21:10 +0100 Subject: [PATCH 05/26] Update Localizable.strings --- fr.lproj/Localizable.strings | 54 ++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 38f2a646..2f1742ef 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -37,8 +37,8 @@ "bluetooth.pin.validation"="Le code pin BLE doit faire 6 chiffres."; "bytes"="Octets"; "cancel"="Annuler"; -"canned.messages"="Messages Canned"; -"canned.messages.config"="Configuration des messages Canned"; +"canned.messages"="Messages préformatés"; +"canned.messages.config"="Configuration des messages préformatés"; "canned.messages.preset.manual"="Configuration manuelle"; "canned.messages.preset.rakrotary"="Module d'encodage rotatif RAK"; "canned.messages.preset.cardkb"="Clavier M5 Stack Card KB / RAK"; @@ -164,31 +164,31 @@ "map.usertrackingmode.none"="Aucun"; "mesh.live.activity"="Activité en direct du maillage"; "mesh.log"="Journal du maillage"; -"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; -"mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; -"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; -"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@"; -"mesh.log.cannedmessages.messages.received %@"="Canned Messages Messages Received For: %@"; -"mesh.log.channel.sent %@ %d"="Sent a Channel for: %@ Channel Index %d"; -"mesh.log.channel.received %d %@"="Channel %d received from: %@"; -"mesh.log.device.config %@"="Device config received: %@"; -"mesh.log.display.config %@"="Display config received: %@"; -"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@"; -"mesh.log.device.metadata.received %@"="Device Metadata received from: %@"; -"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@"; -"mesh.log.externalnotification.config %@"="External Notification module config received: %@"; -"mesh.log.lora.config %@"="LoRa config received: %@"; -"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@"; -"mesh.log.mqtt.config %@"="MQTT module config received: %@"; -"mesh.log.myinfo %@"="MyInfo received: %@"; -"mesh.log.network.config %@"="Network config received: %@"; -"mesh.log.nodeinfo.received %@"="Node info received for: %@"; -"mesh.log.position.config %@"="Positon config received: %@"; -"mesh.log.position.received %@"="Position Packet received from node: %@"; -"mesh.log.rangetest.config %@"="Range Test module config received: %@"; -"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; -"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; -"mesh.log.serial.config %@"="Serial module config received: %@"; +"mesh.log.ambientlighting.config %@"="Configuration du module de lumière ambiante reçue : %@"; +"mesh.log.bluetooth.config %@"="Configuration du Bluetooth reçue : %@"; +"mesh.log.cannedmessage.config %@"="Configuration du module des messages préformatés reçue: %@"; +"mesh.log.cannedmessages.messages.get %@"="Messages du module des messages préformatés demandés pour le noeud : %@"; +"mesh.log.cannedmessages.messages.received %@"="Messages préformatés reçus pour : %@"; +"mesh.log.channel.sent %@ %d"="Canal envoyé pour : %@ Canal index %d"; +"mesh.log.channel.received %d %@"="Canal %d reçu de : %@"; +"mesh.log.device.config %@"="Configuration de l'appareil reçue : %@"; +"mesh.log.display.config %@"="Configuration de l'écran reçue : %@"; +"mesh.log.devicemetadata %@"="Demande des metadatas de l'appareil pour %@"; +"mesh.log.device.metadata.received %@"="Metadatas de l'appareil reçues de : %@"; +"mesh.log.detectionsensor.config %@"="Configuration du module de capteur de détection reçue : %@"; +"mesh.log.externalnotification.config %@"="Configuration du module de notification extérieure reçue : %@"; +"mesh.log.lora.config %@"="Configuration LoRa reçue : %@"; +"mesh.log.lora.config.sent %@"="Configuration LoRa envoyée pour : %@"; +"mesh.log.mqtt.config %@"="Configuration du module MQTT reçue : %@"; +"mesh.log.myinfo %@"="MesInfos reçues : %@"; +"mesh.log.network.config %@"="Configuration du réseau reçue : %@"; +"mesh.log.nodeinfo.received %@"="Information du noeud reçue pour : %@"; +"mesh.log.position.config %@"="Configuration de la position reçue : %@"; +"mesh.log.position.received %@"="Paquet de la position reçu du noeud : %@"; +"mesh.log.rangetest.config %@"="Configuration du module de test de la portée reçue : %@"; +"mesh.log.ringtone.config %@"="Configuration de la sonnerie RTTTL reçue : %@"; +"mesh.log.routing.message %@ %@"="Routage reçu pour la demande d'identité : %@ Status de l'accusé de réception : %@"; +"mesh.log.serial.config %@"="Configuration du moduel série reçue : %@"; "mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; "mesh.log.storeforward.config %@"="Store & Forward module config received: %@"; "mesh.log.telemetry.config %@"="Telemetry module config received: %@"; From a488d4d831a6fe770eb4892acf8eac99a374b29b Mon Sep 17 00:00:00 2001 From: Myrlhyn <161041172+Myrlhyn@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:41:06 +0100 Subject: [PATCH 06/26] Update Localizable.strings --- fr.lproj/Localizable.strings | 70 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 2f1742ef..9240b313 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -187,47 +187,47 @@ "mesh.log.position.received %@"="Paquet de la position reçu du noeud : %@"; "mesh.log.rangetest.config %@"="Configuration du module de test de la portée reçue : %@"; "mesh.log.ringtone.config %@"="Configuration de la sonnerie RTTTL reçue : %@"; -"mesh.log.routing.message %@ %@"="Routage reçu pour la demande d'identité : %@ Status de l'accusé de réception : %@"; -"mesh.log.serial.config %@"="Configuration du moduel série reçue : %@"; -"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; -"mesh.log.storeforward.config %@"="Store & Forward module config received: %@"; -"mesh.log.telemetry.config %@"="Telemetry module config received: %@"; -"mesh.log.telemetry.received %@"="Telemetry received for: %@"; -"mesh.log.textmessage.received"="Message received from the text message app."; -"mesh.log.textmessage.send.failed %@"="Message Send Failed, not properly connected to %@"; -"mesh.log.textmessage.sent %@ %@ %@"="Sent message %@ from %@ to %@"; -"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly."; -"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@"; -"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; -"mesh.log.wantconfig %@"="Issuing Want Config to %@"; -"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; -"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; +"mesh.log.routing.message %@ %@"="Routage reçu pour la demande numéro : %@ Status de l'accusé de réception : %@"; +"mesh.log.serial.config %@"="Configuration du module série reçue : %@"; +"mesh.log.sharelocation %@"="Paquet envoyé avec la position GPS de l'appareil Apple vers le noeud : %@"; +"mesh.log.storeforward.config %@"="Configuration du module de stockage et d'acheminement reçue : %@"; +"mesh.log.telemetry.config %@"="Configuration du module de télémetrie reçue : %@"; +"mesh.log.telemetry.received %@"="Télémetrie reçue pour : %@"; +"mesh.log.textmessage.received"="Message reçu depuis l'application de messagerie texte."; +"mesh.log.textmessage.send.failed %@"="Erreur d'envoi du message, mal connecté à %@"; +"mesh.log.textmessage.sent %@ %@ %@"="Envoi du message %@ de %@ à %@"; +"mesh.log.traceroute.received.direct %@"="La demande de Trace Route envoyée au noeud : %@ a été directement reçue."; +"mesh.log.traceroute.received.route %@"="La demande de Trace Route est revenue : %@"; +"mesh.log.traceroute.sent %@"="Envoi d'une demande de Trace Route au noeud : %@"; +"mesh.log.wantconfig %@"="Envoi d'un Want Config à %@"; +"mesh.log.waypoint.sent %@"="Paquet Waypoint envoyé depuis : %@"; +"mesh.log.waypoint.received %@"="Paquet Waypoint reçu du noeud : %@"; "message"="Message"; -"message.details"="Message Details"; +"message.details"="Détails du message"; "messages"="Messages"; "mode"="Mode"; -"module.configuration"="Module Configuration"; +"module.configuration"="Configuration du module"; "mqtt"="MQTT"; -"mqtt.connect"="Connect to MQTT"; -"mqtt.config"="MQTT Config"; -"mqtt.clientproxy"="MQTT Client Proxy"; -"mqtt.disconnect"="Disconnect from MQTT"; -"mqtt.username"="Username"; -"name"="Name"; -"network"="Network"; -"network.config"="Network Config"; -"nodes"="Nodes"; -"nodes %@"="Nodes (%@)"; -"no.nodes"="No Meshtastic Nodes Found"; -"not.connected"="No device connected"; -"numbers.punctuation"="Numbers and Punctuation"; -"off"="Off"; -"offline"="Offline"; -"on.boot"="On Boot Only"; +"mqtt.connect"="Connecter à MQTT"; +"mqtt.config"="Configuration MQTT"; +"mqtt.clientproxy"="Proxy client MQTT"; +"mqtt.disconnect"="Déconnecter MQTT"; +"mqtt.username"="Nom d'utilisateur"; +"name"="Nom"; +"network"="Réseau"; +"network.config"="Configuration du réseau"; +"nodes"="Noeuds"; +"nodes %@"="Noeuds (%@)"; +"no.nodes"="Aucun noeuds Meshtastic trouvés"; +"not.connected"="Aucun appareil connecté"; +"numbers.punctuation"="Nombres and Ponctuation"; +"off"="Éteint"; +"offline"="Hors ligne"; +"on.boot"="Uniquement au démarrage"; "options"="Options"; -"password"="Password"; +"password"="Mot de passe"; "pause"="Pause"; -"phone.gps"="Phone GPS"; +"phone.gps"="GPS du téléphone"; "phone.gps.interval.description"="How frequently your phone will send your location to the device, location updates to the mesh are managed by the device."; "position"="Position"; "position.config"="Position Config"; From e4f5a0778b85e6069ee22a6726becb1128946522 Mon Sep 17 00:00:00 2001 From: Myrlhyn <161041172+Myrlhyn@users.noreply.github.com> Date: Mon, 26 Feb 2024 21:24:37 +0100 Subject: [PATCH 07/26] Update Localizable.strings --- fr.lproj/Localizable.strings | 132 +++++++++++++++++------------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 9240b313..e6dc0f4c 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -190,7 +190,7 @@ "mesh.log.routing.message %@ %@"="Routage reçu pour la demande numéro : %@ Status de l'accusé de réception : %@"; "mesh.log.serial.config %@"="Configuration du module série reçue : %@"; "mesh.log.sharelocation %@"="Paquet envoyé avec la position GPS de l'appareil Apple vers le noeud : %@"; -"mesh.log.storeforward.config %@"="Configuration du module de stockage et d'acheminement reçue : %@"; +"mesh.log.storeforward.config %@"="Configuration du module Stocker et Transmettre reçue : %@"; "mesh.log.telemetry.config %@"="Configuration du module de télémetrie reçue : %@"; "mesh.log.telemetry.received %@"="Télémetrie reçue pour : %@"; "mesh.log.textmessage.received"="Message reçu depuis l'application de messagerie texte."; @@ -218,7 +218,7 @@ "network.config"="Configuration du réseau"; "nodes"="Noeuds"; "nodes %@"="Noeuds (%@)"; -"no.nodes"="Aucun noeuds Meshtastic trouvés"; +"no.nodes"="Aucun noeud Meshtastic trouvé"; "not.connected"="Aucun appareil connecté"; "numbers.punctuation"="Nombres and Ponctuation"; "off"="Éteint"; @@ -228,78 +228,78 @@ "password"="Mot de passe"; "pause"="Pause"; "phone.gps"="GPS du téléphone"; -"phone.gps.interval.description"="How frequently your phone will send your location to the device, location updates to the mesh are managed by the device."; +"phone.gps.interval.description"="La fréquence à laquelle votre téléphone envoie votre position à l'appareil, les mises à jour de la position vers le maillage sont gérées par l'appareil."; "position"="Position"; -"position.config"="Position Config"; -"preferred.radio"="Preferred Radio"; -"radio.configuration"="Radio Configuration"; -"range.test"="Range Test"; -"range.test.blocked"="Block Range Test"; -"range.test.config"="Range Test Config"; -"reply"="Reply"; -"reboot"="Reboot"; -"reboot.node"="Reboot node?"; -"received.ack"="Received Ack"; -"received.ack.real"="Recipient Ack"; -"resume"="Resume"; -"ringtone"="Ringtone"; -"ringtone.config"="Ringtone Config"; -"route.recorder"="Route Recorder"; +"position.config"="Configuration de la position"; +"preferred.radio"="Radio favorie"; +"radio.configuration"="Configuration de la radio"; +"range.test"="Test de portée"; +"range.test.blocked"="Bloque le test de portée"; +"range.test.config"="Configuration du test de portée"; +"reply"="Répondre"; +"reboot"="Redémarrer"; +"reboot.node"="Redémarrer le noeud ?"; +"received.ack"="Accusé de réception reçu"; +"received.ack.real"="Accusé de réception du destinataire"; +"resume"="Reprendre"; +"ringtone"="Sonnerie"; +"ringtone.config"="Configuration de la sonnerie"; +"route.recorder"="Enregistreur de route"; "routes"="Routes"; -"routing.acknowledged"="Acknowledged"; -"routing.noroute"="No Route"; -"routing.gotnak"="Received a negative acknowledgment"; -"routing.timeout"="Timeout"; -"routing.nointerface"="No Interface"; -"routing.maxretransmit"="Max Retransmission Reached"; -"routing.nochannel"="No Channel"; -"routing.toolarge"="The packet is too large"; -"routing.noresponse"="No Response"; -"routing.dutycyclelimit"="Regional Duty Cycle Limit Reached"; -"routing.badRequest"="Bad Request"; -"routing.notauthorized"="Not Authorized"; +"routing.acknowledged"="Confirmé"; +"routing.noroute"="Pas de route"; +"routing.gotnak"="Accusé de réception négatif reçu"; +"routing.timeout"="Délai d'expiration"; +"routing.nointerface"="Pas d'interface"; +"routing.maxretransmit"="Nombre maximum de retransmissions atteint"; +"routing.nochannel"="Pas de canal"; +"routing.toolarge"="Le paquet est trop grand"; +"routing.noresponse"="Pas de réponse"; +"routing.dutycyclelimit"="Limite du cycle de service régional atteinte"; +"routing.badRequest"="Requête incorrecte"; +"routing.notauthorized"="Non autorisé"; "satellite"="Satellite"; -"satellite.flyover"="Satellite Flyover"; -"save"="Save"; -"save.config %@"="Save Config for %@"; -"serial"="Serial"; -"serial.config"="Serial Config"; -"serial.mode.default"="Default"; +"satellite.flyover"="Pont par satellite"; +"save"="Sauvegarder"; +"save.config %@"="Sauvegarder la configuration pour %@"; +"serial"="Série"; +"serial.config"="Configuration série"; +"serial.mode.default"="Défaut"; "serial.mode.simple"="Simple"; "serial.mode.proto"="Protobufs"; -"serial.mode.txtmsg"="Text Message"; -"serial.mode.nmea"="NMEA Positions"; -"settings"="Settings"; -"share.channels"="Share QR Code"; -"share.position"="Share Position"; -"subscribed"="Subscribed to mesh"; -"select.contact"="Select a Contact"; -"select.node"="Select a Node"; -"select.menu.item"="Select an item from the menu"; -"set.region"="Set LoRa Region"; +"serial.mode.txtmsg"="Message texte"; +"serial.mode.nmea"="Positions NMEA"; +"settings"="Réglages"; +"share.channels"="Partager le QR Code"; +"share.position"="Partager la position"; +"subscribed"="Abonné au maillage"; +"select.contact"="Sélectioner un contact"; +"select.node"="Sélectioner un noeud"; +"select.menu.item"="Sélectioner un item du menu"; +"set.region"="Définir la région LoRa"; "standard"="Standard"; -"standard.muted"="Standard Muted"; -"start"="Start"; -"storeforward"="Store & Forward"; -"storeforward.config"="Store & Forward Config"; -"storeforward.heartbeat"="Send Heartbeat"; +"standard.muted"="Standard en sourdine"; +"start"="Démarrer"; +"storeforward"="Stocker et Transmettre"; +"storeforward.config"="Configuration de Stocker et Transmettre"; +"storeforward.heartbeat"="Envoyer une impulsion"; "ssid"="SSID"; -"tapback"="Tapback Response"; -"tapback.heart"="Heart"; -"tapback.thumbsup"="Thumbs Up"; -"tapback.thumbsdown"="Thumbs Down"; +"tapback"="Réponse de Tapback"; +"tapback.heart"="Coeur"; +"tapback.thumbsup"="Pouce levé"; +"tapback.thumbsdown"="Pouce baissé"; "tapback.haha"="HaHa"; -"tapback.exclamation"="Exclamation Mark"; -"tapback.question"="Question Mark"; -"tapback.poop"="Poop"; -"telemetry"="Telemetry (Sensors)"; -"telemetry.config"="Telemetry Config"; -"timeout"="Timeout"; -"timestamp"="Timestamp"; -"tip.bluetooth.connect.title"="Connected Radio"; -"tip.bluetooth.connect.message"="Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; -"tip.channels.create.title"="Manage Channels"; -"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)"; +"tapback.exclamation"="Point d'exclamation"; +"tapback.question"="Point d'interrogation"; +"tapback.poop"="Caca"; +"telemetry"="Télémetrie (Capteurs)"; +"telemetry.config"="Configuration de télémetrie"; +"timeout"="Délai d'expiration"; +"timestamp"="Horodatage"; +"tip.bluetooth.connect.title"="Radio connectée"; +"tip.bluetooth.connect.message"="Affiche les informations de la radio Lora connectée via le bluetooth. Vous pouvez faire un glissé vers la gauche pour déconnecter la radio et un appui long pour voir les statistiques ou démarrer l'activité en direct."; +"tip.channels.create.title"="Gérer les canaux"; +"tip.channels.create.message"="La pluspart des données de votre maillage sont envoyées sur le canal principal. Vous pouvez définir des canaux secondaires pour créer des groupes de messagerie additionnelle sécurisés avec leur propre clée. [Conseils de configuration du canal](https://meshtastic.org/docs/configuration/tips/)"; "tip.channels.share.title"="Sharing Meshtastic Channels"; "tip.channels.share.message"="A Meshtastic QR code contains the LoRa config and channel values needed to communicate. Most mesh activity takes place on the required Primary channel. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. Other channels are for private groups, each with its own key."; "tip.messages.title"="Messages"; From 0377552ed962a5493ee2ab328e33f15f57db50f4 Mon Sep 17 00:00:00 2001 From: Myrlhyn <161041172+Myrlhyn@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:02:00 +0100 Subject: [PATCH 08/26] Update Localizable.strings --- fr.lproj/Localizable.strings | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index e6dc0f4c..c1a6b775 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -299,18 +299,18 @@ "tip.bluetooth.connect.title"="Radio connectée"; "tip.bluetooth.connect.message"="Affiche les informations de la radio Lora connectée via le bluetooth. Vous pouvez faire un glissé vers la gauche pour déconnecter la radio et un appui long pour voir les statistiques ou démarrer l'activité en direct."; "tip.channels.create.title"="Gérer les canaux"; -"tip.channels.create.message"="La pluspart des données de votre maillage sont envoyées sur le canal principal. Vous pouvez définir des canaux secondaires pour créer des groupes de messagerie additionnelle sécurisés avec leur propre clée. [Conseils de configuration du canal](https://meshtastic.org/docs/configuration/tips/)"; -"tip.channels.share.title"="Sharing Meshtastic Channels"; -"tip.channels.share.message"="A Meshtastic QR code contains the LoRa config and channel values needed to communicate. Most mesh activity takes place on the required Primary channel. If you don't share your primary channel your first shared channel becomes the primary channel on the other network. Other channels are for private groups, each with its own key."; +"tip.channels.create.message"="La pluspart des données de votre maillage sont envoyées sur le canal principal. Vous pouvez définir des canaux secondaires pour créer des groupes de messagerie additionnelle sécurisés avec leur propre clé. [Conseils de configuration du canal](https://meshtastic.org/docs/configuration/tips/)"; +"tip.channels.share.title"="Partage des canaux Meshtastic"; +"tip.channels.share.message"="Un code QR Meshtastic contient la configuration LoRa et les valeurs de canal nécessaires pour communiquer. La plupart des activités du maillage ont lieu sur le canal principal requis. Si vous ne partagez pas votre canal principal, votre premier canal partagé devient le canal principal de l’autre réseau. Les autres canaux sont pour les groupes privés, chacun avec sa propre clé."; "tip.messages.title"="Messages"; -"tip.messages.message"="You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details."; +"tip.messages.message"="Vous pouvez envoyer et recevoir des canaux (chats de groupe) et des messages directs. À partir de n’importe quel message, vous pouvez faire un appui long pour voir les actions possibles comme copier, répondre, tapback et supprimer ainsi que les détails de l'envoi."; "twitter"="Twitter"; -"unknown"="Unknown"; -"unknown.age"="Unknown Age"; -"unset"="Unset"; -"update.firmware"="Update Your Firmware"; -"update.interval"="Update Interval"; -"user"="User"; -"user.details"="User Details"; -"voltage"="Voltage"; -"waiting"="Waiting. . ."; +"unknown"="Inconnu"; +"unknown.age"="Age inconnu"; +"unset"="Désactivé"; +"update.firmware"="Mettre à jour votre Firmware"; +"update.interval"="Intervale de mise à jour"; +"user"="Utilisateur"; +"user.details"="Détails de l'utilisateur"; +"voltage"="Tension"; +"waiting"="En attente . . ."; From d8b4ffd5b97e37055c951d78b9fdd2b5b690610a Mon Sep 17 00:00:00 2001 From: Myrlhyn <161041172+Myrlhyn@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:40:52 +0100 Subject: [PATCH 09/26] Update Localizable.strings Last review --- fr.lproj/Localizable.strings | 58 ++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index c1a6b775..4a5ea20d 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -8,7 +8,7 @@ "about"="À propos"; "about.meshtastic"="À propos de Meshtastic"; "admin"="Administrateur"; -"admin.log"="Journal des messages administrateur"; +"admin.log"="Journal des messages d'administration"; "ago"="auparavant"; "airtime"="Temps d'émission"; "always.on"="En permanence"; @@ -24,17 +24,17 @@ "battery.level"="Niveau de batterie"; "ble.name"="Nom du BLE"; "ble.connection.timeout %d %@"="Connexion impossible après %d essais avec %@. Allez dans Réglages > Bluetooth et essayez de faire de faire > Oublier cet appareil."; -"ble.errorcode.6 %@"="%@ L'application se reconnectera automatiquement à la radio en favori dès qu'elle sera à nouveau visibile."; -"ble.errorcode.14 %@"="%@ Cette erreur ne peut généralement pas être corrigée sans aller dans Réglages > Bluetooth et faire > Oublier cet appareil, puis de reconnecter la radio."; +"ble.errorcode.6 %@"="%@ L'application se reconnectera automatiquement à la radio en favori dès qu'elle sera à nouveau disponible."; +"ble.errorcode.14 %@"="%@ Cette erreur ne peut généralement pas être corrigée sans aller dans Réglages > Bluetooth et faire > Oublier cet appareil, puis reconnecter la radio."; "ble.errorcode.pin %@"="%@ Merci d'essayer à nouveau en vérifiant bien le code PIN."; "bluetooth"="Bluetooth"; -"bluetooth.off"="Le Bluetooth is arrêté"; -"bluetooth.config"="Configuration du Bluetooth"; +"bluetooth.off"="Le Bluetooth est arrêté"; +"bluetooth.config"="Configuration Bluetooth"; "bluetooth.mode.randompin"="Code PIN aléatoire"; "bluetooth.mode.fixedpin"="Code PIN fixe"; -"bluetooth.mode.nopin"="Sans copde PIN (connexion directe)"; +"bluetooth.mode.nopin"="Sans code PIN (connexion directe)"; "bluetooth.pairingmode"="Mode d'appairage"; -"bluetooth.pin.validation"="Le code pin BLE doit faire 6 chiffres."; +"bluetooth.pin.validation"="Le code pin BLE doit avoir 6 chiffres."; "bytes"="Octets"; "cancel"="Annuler"; "canned.messages"="Messages préformatés"; @@ -69,7 +69,7 @@ "device.config"="Configuration de l'appareil"; "device.metrics.delete"="Effacer toutes les mesures de l’appareil?"; "device.metrics.log"="Journal des mesures de l'appareil"; -"device.role.client"="Apploication connectée ou appareil de messagerie indépendant."; +"device.role.client"="Application connectée ou appareil de messagerie autonome."; "device.role.clientmute"="Appareil ne transmettant pas les paquets provenant d'autres appareils."; "device.role.clienthidden"="Appareil ne diffusant que si nécessaire pour la discrétion et l'économie d'énergie."; "device.role.tracker"="Transmet les paquets de positions GPS en priorité."; @@ -106,10 +106,10 @@ "gpsmode.disabled"="Désactivé"; "gpsmode.enabled"="Activé"; "gpsmode.notPresent"="Absent"; -"heard"="Entendre"; -"heard.last"="Entendu pour la dernière fois"; +"heard"="Capté"; +"heard.last"="Capté pour la dernière fois"; "hybrid"="Hybride"; -"hybrid.flyover"="Survol hybride"; +"hybrid.flyover"="Flyover hybride"; "include"="Inclure"; "inputevent.none"="Aucun"; "inputevent.up"="Haut"; @@ -126,10 +126,10 @@ "interval.five.seconds"="Cinq secondes"; "interval.ten.seconds"="Dix secondes"; "interval.fifteen.seconds"="Quinze secondes"; -"interval.twenty.seconds"="Vingr secondes"; -"interval.twentyfive.seconds"="Vignt cinq secondes"; +"interval.twenty.seconds"="Vingt secondes"; +"interval.twentyfive.seconds"="Vingt cinq secondes"; "interval.thirty.seconds"="Trente secondes"; -"interval.fortyfive.seconds"="Quanrante cinq secondes"; +"interval.fortyfive.seconds"="Quarante cinq secondes"; "interval.one.minute"="Une minute"; "interval.two.minutes"="Deux minutes"; "interval.five.minutes"="Cinq minutes"; @@ -164,37 +164,37 @@ "map.usertrackingmode.none"="Aucun"; "mesh.live.activity"="Activité en direct du maillage"; "mesh.log"="Journal du maillage"; -"mesh.log.ambientlighting.config %@"="Configuration du module de lumière ambiante reçue : %@"; -"mesh.log.bluetooth.config %@"="Configuration du Bluetooth reçue : %@"; -"mesh.log.cannedmessage.config %@"="Configuration du module des messages préformatés reçue: %@"; -"mesh.log.cannedmessages.messages.get %@"="Messages du module des messages préformatés demandés pour le noeud : %@"; +"mesh.log.ambientlighting.config %@"="Configuration du module lumière ambiante reçue : %@"; +"mesh.log.bluetooth.config %@"="Configuration Bluetooth reçue : %@"; +"mesh.log.cannedmessage.config %@"="Configuration du module messages préformatés reçue : %@"; +"mesh.log.cannedmessages.messages.get %@"="Messages du module messages préformatés demandés pour le noeud : %@"; "mesh.log.cannedmessages.messages.received %@"="Messages préformatés reçus pour : %@"; "mesh.log.channel.sent %@ %d"="Canal envoyé pour : %@ Canal index %d"; "mesh.log.channel.received %d %@"="Canal %d reçu de : %@"; "mesh.log.device.config %@"="Configuration de l'appareil reçue : %@"; "mesh.log.display.config %@"="Configuration de l'écran reçue : %@"; -"mesh.log.devicemetadata %@"="Demande des metadatas de l'appareil pour %@"; +"mesh.log.devicemetadata %@"="Demande des metadatas de l'appareil à %@"; "mesh.log.device.metadata.received %@"="Metadatas de l'appareil reçues de : %@"; -"mesh.log.detectionsensor.config %@"="Configuration du module de capteur de détection reçue : %@"; -"mesh.log.externalnotification.config %@"="Configuration du module de notification extérieure reçue : %@"; +"mesh.log.detectionsensor.config %@"="Configuration du module capteur de détection reçue : %@"; +"mesh.log.externalnotification.config %@"="Configuration du module notification extérieure reçue : %@"; "mesh.log.lora.config %@"="Configuration LoRa reçue : %@"; -"mesh.log.lora.config.sent %@"="Configuration LoRa envoyée pour : %@"; +"mesh.log.lora.config.sent %@"="Configuration LoRa envoyée à : %@"; "mesh.log.mqtt.config %@"="Configuration du module MQTT reçue : %@"; "mesh.log.myinfo %@"="MesInfos reçues : %@"; "mesh.log.network.config %@"="Configuration du réseau reçue : %@"; "mesh.log.nodeinfo.received %@"="Information du noeud reçue pour : %@"; "mesh.log.position.config %@"="Configuration de la position reçue : %@"; "mesh.log.position.received %@"="Paquet de la position reçu du noeud : %@"; -"mesh.log.rangetest.config %@"="Configuration du module de test de la portée reçue : %@"; +"mesh.log.rangetest.config %@"="Configuration du module test deportée reçue : %@"; "mesh.log.ringtone.config %@"="Configuration de la sonnerie RTTTL reçue : %@"; "mesh.log.routing.message %@ %@"="Routage reçu pour la demande numéro : %@ Status de l'accusé de réception : %@"; "mesh.log.serial.config %@"="Configuration du module série reçue : %@"; "mesh.log.sharelocation %@"="Paquet envoyé avec la position GPS de l'appareil Apple vers le noeud : %@"; "mesh.log.storeforward.config %@"="Configuration du module Stocker et Transmettre reçue : %@"; -"mesh.log.telemetry.config %@"="Configuration du module de télémetrie reçue : %@"; +"mesh.log.telemetry.config %@"="Configuration du module télémetrie reçue : %@"; "mesh.log.telemetry.received %@"="Télémetrie reçue pour : %@"; "mesh.log.textmessage.received"="Message reçu depuis l'application de messagerie texte."; -"mesh.log.textmessage.send.failed %@"="Erreur d'envoi du message, mal connecté à %@"; +"mesh.log.textmessage.send.failed %@"="Erreur d'envoi du message, mauvaise connexion à %@"; "mesh.log.textmessage.sent %@ %@ %@"="Envoi du message %@ de %@ à %@"; "mesh.log.traceroute.received.direct %@"="La demande de Trace Route envoyée au noeud : %@ a été directement reçue."; "mesh.log.traceroute.received.route %@"="La demande de Trace Route est revenue : %@"; @@ -211,7 +211,7 @@ "mqtt.connect"="Connecter à MQTT"; "mqtt.config"="Configuration MQTT"; "mqtt.clientproxy"="Proxy client MQTT"; -"mqtt.disconnect"="Déconnecter MQTT"; +"mqtt.disconnect"="Déconnecter le MQTT"; "mqtt.username"="Nom d'utilisateur"; "name"="Nom"; "network"="Réseau"; @@ -234,7 +234,7 @@ "preferred.radio"="Radio favorie"; "radio.configuration"="Configuration de la radio"; "range.test"="Test de portée"; -"range.test.blocked"="Bloque le test de portée"; +"range.test.blocked"="Test de portée bloqué"; "range.test.config"="Configuration du test de portée"; "reply"="Répondre"; "reboot"="Redémarrer"; @@ -259,7 +259,7 @@ "routing.badRequest"="Requête incorrecte"; "routing.notauthorized"="Non autorisé"; "satellite"="Satellite"; -"satellite.flyover"="Pont par satellite"; +"satellite.flyover"="Flyover par satellite"; "save"="Sauvegarder"; "save.config %@"="Sauvegarder la configuration pour %@"; "serial"="Série"; @@ -303,7 +303,7 @@ "tip.channels.share.title"="Partage des canaux Meshtastic"; "tip.channels.share.message"="Un code QR Meshtastic contient la configuration LoRa et les valeurs de canal nécessaires pour communiquer. La plupart des activités du maillage ont lieu sur le canal principal requis. Si vous ne partagez pas votre canal principal, votre premier canal partagé devient le canal principal de l’autre réseau. Les autres canaux sont pour les groupes privés, chacun avec sa propre clé."; "tip.messages.title"="Messages"; -"tip.messages.message"="Vous pouvez envoyer et recevoir des canaux (chats de groupe) et des messages directs. À partir de n’importe quel message, vous pouvez faire un appui long pour voir les actions possibles comme copier, répondre, tapback et supprimer ainsi que les détails de l'envoi."; +"tip.messages.message"="Vous pouvez envoyer et recevoir des canaux (chats de groupe) et des messages directs. Depuis n’importe quel message, vous pouvez faire un appui long pour voir les actions possibles comme copier, répondre, tapback et supprimer ainsi que les détails de l'envoi."; "twitter"="Twitter"; "unknown"="Inconnu"; "unknown.age"="Age inconnu"; From 4ce39f6b2508aadf83e58e727ebcda0ed78408bf Mon Sep 17 00:00:00 2001 From: xerion79585 <82042289+xerion79585@users.noreply.github.com> Date: Sat, 2 Mar 2024 12:47:24 +0800 Subject: [PATCH 10/26] Add files via upload --- zh-TW.lproj/Localizable.strings | 333 ++++++++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 zh-TW.lproj/Localizable.strings diff --git a/zh-TW.lproj/Localizable.strings b/zh-TW.lproj/Localizable.strings new file mode 100644 index 00000000..6f36c512 --- /dev/null +++ b/zh-TW.lproj/Localizable.strings @@ -0,0 +1,333 @@ +/* + Localizable.strings + Meshtastic + + Created by BM6HIP on 2024/3/2 + +*/ +"about"="關於"; +"about.meshtastic"="關於 Meshtastic"; +"admin"="管理員"; +"admin.log"="管理員消息紀錄檔"; +"ago"="ago"; +"airtime"="廣播時間"; +"always.on"="常亮"; +"ambient.lighting"="Ambient Lighting"; +"ambient.lighting.config"="Ambient Lighting Config"; +"appsettings"="設定"; +"appsettings.provide.location"="提供定位到 Mesh 網路"; +"appsettings.smartposition"="Smart Position"; +"are.you.sure"="是否確定?"; +"ascii.capable"="ASCII Capable"; +"available.radios"="可以連接的設備"; +"automatic.detection"="自動識別"; +"battery.level"="電池電量"; +"ble.name"="藍芽名稱"; +"ble.connection.timeout %d %@"="嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該電台。"; +"ble.errorcode.6 %@"="%@ 如果在首選電台的旁邊,App 將會自動重連。"; +"ble.errorcode.14 %@"="%@ 這個錯誤通常無法自動修復,你需要在系統設定的藍芽選項中忽略該電台並重新配對。"; +"ble.errorcode.pin %@"="%@ 請再次嘗試連接並仔細檢查 PIN 碼。"; +"bluetooth"="藍芽"; +"bluetooth.off"="藍芽已關閉"; +"bluetooth.config"="藍芽設置"; +"bluetooth.mode.randompin"="隨機 PIN 碼"; +"bluetooth.mode.fixedpin"="固定 PIN 碼"; +"bluetooth.mode.nopin"="不使用 PIN 碼(直接配對)"; +"bluetooth.pairingmode"="配對模式"; +"bluetooth.pin.validation"="藍芽 PIN 碼必須是 6 位數字。"; +"bytes"="字節"; +"cancel"="取消"; +"canned.messages"="通知"; +"canned.messages.config"="通知設定"; +"canned.messages.preset.manual"="手動設定"; +"canned.messages.preset.rakrotary"="RAK 旋轉編碼器"; +"canned.messages.preset.cardkb"="M5Stack 卡片鍵盤 / RAK 鍵盤"; +"channel"="頻道"; +"channel.role.disabled"="關閉"; +"channel.role.primary"="主要"; +"channel.role.secondary"="次要"; +"channel.utilization"="頻道利用率"; +"channels"="頻道"; +"clear.app.data"="清除 App 資料"; +"clear.log"="清除紀錄檔"; +"close"="關閉"; +"config.power.settings"="電源"; +"config.power.title"="電源設定"; +"config.power.section.battery"="電池"; +"config.power.section.sleep"="休眠"; +"config.power.adc.override"="ADC Override"; +"config.power.adc.multiplier"="Multiplier"; +"config.power.ls.secs"="Light Sleep Interval"; +"config.power.min.wake.secs"="最小的喚醒間隔時間"; +"config.power.saving"="省電模式"; +"config.power.saving.description"="Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button."; +"config.power.shutdown.on.power.loss"="失去電源後關機"; +"config.power.shutdown.after.secs"="之後"; +"config.power.wait.bluetooth.secs"="等待藍芽"; +"config.ringtone"="RTTTL Ringtone"; +"config.ringtone.title"="鈴聲"; +"config.ringtone.label"="Ringtone Transfer Language"; +"config.ringtone.description"="Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications."; +"config.module.paxcounter.settings"="PAX Counter"; +"config.module.paxcounter.title"="PAX Counter Config"; +"config.module.paxcounter.enabled.description"="When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be enabled for PAX counter to work."; +"config.module.paxcounter.updateinterval"="Update Interval"; +"config.module.paxcounter.updateinterval.description"="How often we can send a message to the mesh when people are detected."; +"config.save.confirm"="電台將會在設定儲存後重啟。"; +"connected.radio"="已連接的電台"; +"communicating"="與電台進行通訊中..."; +"connected"="已連接"; +"connecting"="連接中..."; +"contacts"="聯絡人"; +"contacts %@"="聯絡人 (%@)"; +"copy"="複製"; +"current"="目前"; +"default"="預設"; +"delete"="刪除"; +"detection.sensor"="檢測感測器"; +"device"="設備"; +"device.config"="電台設定"; +"device.configuration"="設備設定"; +"device.metrics.delete"="刪除所有電台指標??"; +"device.metrics.log"="電台指標紀錄檔"; +"device.role.client"="標準模式 - App 可以連接到電台進行收發操作,並且會自動轉發 Mesh 網路中其他中繼點的消息。"; +"device.role.clientmute"="靜音模式 - 與標準模式類似,App 可以連接到電台進行收發操作,但不會轉發 Mesh 網路中其他中繼點的消息。"; +"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption."; +"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\""; +"device.role.router"="纯路由模式 - 自動轉發 Mesh 網路中其他中繼點的消息,中繼模式下螢幕會熄滅,Wi-Fi 和藍芽將會進入睡眠模式,App 將無法連接到電台進行收發操作。"; +"device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼點的消息,App 也可以連接到電台進行收發操作。"; +"device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。"; +"device.role.tracker"="追蹤模式 - 用於作為 GPS 追蹤器。從該設備發送的定位數據包優先級較高,每兩分鐘廣播一次。智能位置廣播預設為關閉。"; +"direct.messages"="聊天"; +"dismiss.keyboard"="隱藏鍵盤"; +"display"="螢幕(電台螢幕)"; +"display.config"="螢幕設定"; +"distance"="距離"; +"disconnect"="斷開連接"; +"echo"="echo"; +"email.address"="電子信箱"; +"enabled"="啟用"; +"encrypted"="加密"; +"external.notification"="外部通知"; +"external.notification.config"="外部通知設定"; +"finish"="完成"; +"firmware.version"="韌體版本"; +"firmware.version.unsupported"="檢測到不支援的韌體版本,無法連接到電台。"; +"gas"="Gas"; +"gas.resistance"="Gas Resistance"; +"generate.qr.code"="生成QRcode"; +"gpsformat.dec"="十進制"; +"gpsformat.dms"="度分秒"; +"gpsformat.utm"="通用橫軸墨卡托投影"; +"gpsformat.mgrs"="軍事網格系統"; +"gpsformat.olc"="開放的位置代碼(又稱加碼)"; +"gpsformat.osgr"="英國國土測量局網格"; +"heard"="收到"; +"heard.last"="最後收到"; +"hybrid"="混合"; +"hybrid.flyover"="混合視圖"; +"include"="包含"; +"inputevent.none"="無"; +"inputevent.up"="上"; +"inputevent.down"="下"; +"inputevent.left"="左"; +"inputevent.right"="右"; +"inputevent.select"="選擇"; +"inputevent.back"="返回"; +"inputevent.cancel"="取消"; +"interval.one.second"="一秒"; +"interval.two.seconds"="兩秒"; +"interval.three.seconds"="三秒"; +"interval.four.seconds"="四秒"; +"interval.five.seconds"="五秒"; +"interval.ten.seconds"="十秒"; +"interval.fifteen.seconds"="十五秒"; +"interval.twenty.seconds"="二十秒"; +"interval.twentyfive.seconds"="二十五秒"; +"interval.thirty.seconds"="三十秒"; +"interval.fortyfive.seconds"="四十五秒"; +"interval.one.minute"="一分鐘"; +"interval.two.minutes"="兩分鐘"; +"interval.five.minutes"="五分鐘"; +"interval.ten.minutes"="十分鐘"; +"interval.fifteen.minutes"="十五分鐘"; +"interval.thirty.minutes"="三十分鐘"; +"interval.one.hour"="一小時"; +"interval.two.hours"="兩小時"; +"interval.three.hours"="三小時"; +"interval.four.hours"="四小時"; +"interval.five.hours"="五小時"; +"interval.six.hours"="六小時"; +"interval.twelve.hours"="十二小時"; +"interval.eighteen.hours"="十八小時"; +"interval.twentyfour.hours"="二十四小時"; +"interval.thirtysix.hours"="三十六小時"; +"interval.tyeight.hours"="四十八小时小時"; +"interval.eventytwo.hours"="七十二小時"; +"keyboard.type"="鍵盤類型"; +"logging"="加載中"; +"lora"="LoRa"; +"lora.config"="LoRa 設定"; +"map"="Mesh 地圖"; +"map.centering"="居中"; +"map.tiles.delete"="刪除已緩存的地圖區塊"; +"map.recentering"="自動重新居中"; +"map.use.legacy"="Use Legacy Mesh Map"; +"map.type"="地圖類型"; +"map.usertrackingmode"="使用者跟隨模式"; +"map.usertrackingmode.none"="無"; +"map.usertrackingmode.follow"="跟隨"; +"map.usertrackingmode.followwithheading"="Follow with heading"; +"mesh.live.activity"="Mesh 即時活動"; +"mesh.log"="Mesh 紀錄檔"; +"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; +"mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; +"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; +"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@"; +"mesh.log.cannedmessages.messages.received %@"="Canned Messages Messages Received For: %@"; +"mesh.log.channel.sent %@ %d"="Sent a Channel for: %@ Channel Index %d"; +"mesh.log.channel.received %d %@"="Channel %d received from: %@"; +"mesh.log.device.config %@"="收到裝置設定: %@"; +"mesh.log.display.config %@"="收到顯示模組設定: %@"; +"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@"; +"mesh.log.device.metadata.received %@"="Device Metadata admin message received from: %@"; +"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@"; +"mesh.log.externalnotification.config %@"="External Notification module config received: %@"; +"mesh.log.lora.config %@"="收到LoRa設定: %@"; +"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@"; +"mesh.log.mqtt.config %@"="MQTT module config received: %@"; +"mesh.log.myinfo %@"="MyInfo received: %@"; +"mesh.log.network.config %@"="收到網路設定: %@"; +"mesh.log.nodeinfo.received %@"="收到中繼點訊息: %@"; +"mesh.log.paxcounter %@"="PAX Counter message received for: %@"; +"mesh.log.position.config %@"="Positon config received: %@"; +"mesh.log.position.received %@"="從中繼點接收到定位封包: %@"; +"mesh.log.rangetest.config %@"="收到拉距測試模組設定: %@"; +"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; +"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; +"mesh.log.serial.config %@"="Serial module config received: %@"; +"mesh.log.sharelocation %@"="傳送iOS裝置的GPS定位封包到中繼點上: %@"; +"mesh.log.storeforward.config %@"="Store & Forward module config received: %@"; +"mesh.log.telemetry.config %@"="收到遠測模組設定: %@"; +"mesh.log.telemetry.received %@"="收到遠測資料: %@"; +"mesh.log.textmessage.received"="Message received from the text message app."; +"mesh.log.textmessage.send.failed %@"="訊息傳送失敗, 沒有正確連接到 %@"; +"mesh.log.textmessage.sent %@ %@ %@"="傳送訊息 %@ 從 %@ 到 %@"; +"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly."; +"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@"; +"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; +"mesh.log.wantconfig %@"="Issuing Want Config to %@"; +"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; +"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; +"message"="訊息"; +"message.details"="詳細訊息"; +"messages"="訊息"; +"mode"="模式"; +"module.configuration"="模塊設定"; +"mqtt"="MQTT"; +"mqtt.connect"="Connect to MQTT"; +"mqtt.config"="MQTT 設定"; +"mqtt.clientproxy"="MQTT 客户端代理"; +"mqtt.disconnect"="Disconnect from MQTT"; +"mqtt.username"="用戶名稱"; +"name"="名稱"; +"network"="網路"; +"network.config"="網路設定"; +"nodes"="中繼點"; +"nodes %@"="中繼點 (%@)"; +"no.nodes"="未找到 Meshtastic 中繼點"; +"not.connected"="未連接到電台"; +"numbers.punctuation"="數字和標點符號"; +"off"="關閉"; +"offline"="離線"; +"on.boot"="只在啟動時"; +"options"="選項"; +"password"="密碼"; +"pause"="暫停"; +"phone.gps"="手機 GPS"; +"phone.gps.interval.description"="電台通過手機獲得定位的時間間隔,但是向 Mesh 網路中更新定位的時間間隔由電台控制。"; +"position"="定位"; +"position.config"="定位設定"; +"preferred.radio"="首選電台"; +"radio.configuration"="電台設定"; +"range.test"="拉距測試"; +"range.test.blocked"="區塊範圍測試"; +"range.test.config"="拉距測試設定"; +"reply"="回復"; +"reboot"="重新啟動"; +"reboot.node"="重啟中繼點"; +"received.ack"="收到確認"; +"received.ack.real"="收件人確認"; +"resume"="恢復"; +"ringtone"="鈴聲"; +"ringtone.config"="鈴聲設定"; +"route.recorder"="路線錄製"; +"routes"="路線"; +"routing.acknowledged"="確認"; +"routing.noroute"="找不到目標"; +"routing.gotnak"="收到否認"; +"routing.timeout"="逾時"; +"routing.nointerface"="無連接"; +"routing.maxretransmit"="已達到最大重試次數"; +"routing.nochannel"="没有頻道"; +"routing.toolarge"="數據包過大"; +"routing.noresponse"="無回應"; +"routing.dutycyclelimit"="已達到物錢區域循環週期發射上限"; +"routing.badRequest"="錯誤請求"; +"routing.notauthorized"="未授權"; +"satellite"="衛星"; +"satellite.flyover"="衛星識圖"; +"save"="儲存"; +"save.config %@"="儲存%@的設定"; +"serial"="串口"; +"serial.config"="串口設定"; +"serial.mode.default"="預設"; +"serial.mode.simple"="簡單"; +"serial.mode.proto"="Protobufs"; +"serial.mode.txtmsg"="文本訊息"; +"serial.mode.nmea"="NMEA 位置"; +"settings"="設定"; +"share.channels"="分享頻道QRcode"; +"share.position"="分享位置"; +"subscribed"="連接到 Mesh 網路"; +"select.contact"="選擇聯絡人"; +"select.node"="選擇中繼點"; +"select.menu.item"="從菜單選擇項目"; +"set.region"="設定 LoRa 區域"; +"standard"="標準"; +"standard.muted"="標準靜音"; +"start"="開始"; +"ssid"="SSID"; +"storeforward"="儲存 & 轉發"; +"storeforward.config"="儲存 & 轉發設定"; +"storeforward.heartbeat"="發送心跳包"; +"tapback"="響應"; +"tapback.heart"="心"; +"tapback.thumbsup"="豎大拇指"; +"tapback.thumbsdown"="倒大拇指"; +"tapback.haha"="哈哈"; +"tapback.exclamation"="驚嘆號"; +"tapback.question"="問號"; +"tapback.poop"="便便"; +"telemetry"="遠測(傳感器)"; +"telemetry.config"="遠側設定"; +"timeout"="超時"; +"timestamp"="時間戳記"; +"tip.bluetooth.connect.title"="連接到 LoRa 電台"; +"tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 電台的信息。您可以向左滑動斷開電台,長按查看統計訊息或開始即時活動。"; +"tip.channels.create.title"="管理頻道"; +"tip.channels.create.message"="現在 Mesh 上的資料會通過主通道發送。您可以設定輔助通道來建立由自己的金鑰保護的其他訊息組 [頻道設定提示](https://meshtastic.org/docs/configuration/radio/channels/)"; +"tip.channels.share.title"="共享 Meshtastic 頻道"; +"tip.channels.share.message"="在 Meshtastic 網路中最多有 8 個頻道。第一個頻道是主頻道,大多數活動都發生在這裡,也是必需的。如果您不共享主頻道,您的第一個共享頻道就會成為其他網路的主頻道。它會在其主頻道和您的輔助頻道上對話。名稱為 admin 的頻道可遠端控制中繼點。其他頻道用於私人群组,每個群組都有自己的密鑰。"; +"tip.messages.title"="消息"; +"tip.messages.message"="您可以發送和接收1對1聊天和群聊。在任何訊息中,您都可以長按查看可用的操作,如複製、回復、拍一拍、刪除以及詳情。"; +"twitter"="Twitter"; +"unknown"="未知"; +"unknown.age"="未知時間"; +"unset"="未設置"; +"update.firmware"="更新韌體"; +"update.interval"="更新間隔"; +"user"="使用者"; +"user.details"="使用者資料"; +"voltage"="電壓"; +"waiting"="等待中..."; From 35f99173ba549d94cbb35535eecbd886bbe01630 Mon Sep 17 00:00:00 2001 From: Wolfgang Nagele Date: Mon, 11 Mar 2024 21:41:44 +0100 Subject: [PATCH 11/26] Device metrics chart improvements --- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index e1e87b6a..269b3020 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -34,22 +34,25 @@ struct DeviceMetricsLog: View { Chart { ForEach(chartData, id: \.self) { point in - Plot { - LineMark( - x: .value("x", point.time!), - y: .value("y", point.batteryLevel) - ) + if point.batteryLevel != 101 { // Filter out magic charging value + 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(.linear) } - .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) ) + .symbolSize(25) } .accessibilityLabel("Line Series") .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") @@ -64,6 +67,7 @@ struct DeviceMetricsLog: View { x: .value("x", point.time!), y: .value("y", point.airUtilTx) ) + .symbolSize(25) } .accessibilityLabel("Line Series") .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") From 50add85292423c48aa4ed092a8ff7f4e2bd12d5a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 13 Mar 2024 05:52:31 -0700 Subject: [PATCH 12/26] Tighten up node list Add m to dBm on lora config New reverse geocoded nearby mqtt topics --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Views/Bluetooth/Connect.swift | 2 +- .../Views/Nodes/Helpers/NodeListItem.swift | 4 +- .../Views/Settings/Config/LoRaConfig.swift | 2 +- .../Settings/Config/Module/MQTTConfig.swift | 81 +++++++++++++++++-- 5 files changed, 80 insertions(+), 13 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6cf4d0e2..a0beff25 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1572,7 +1572,7 @@ CODE_SIGN_ENTITLEMENTS = Meshtastic/Meshtastic.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 842; + CURRENT_PROJECT_VERSION = 843; DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -1606,7 +1606,7 @@ CODE_SIGN_ENTITLEMENTS = Meshtastic/Meshtastic.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 842; + CURRENT_PROJECT_VERSION = 843; DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index abf933a6..8fcc31cb 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -209,7 +209,7 @@ struct Connect: View { }.padding([.bottom, .top]) } } - .confirmationDialog("Connecting to a new radio will clear all local app data on the phone and will reset all app settings.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { + .confirmationDialog("Connecting to a new radio will clear all local app data on the phone and will reset all app specific settings.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { Button("Connect to new radio?", role: .destructive) { UserDefaults.preferredPeripheralId = selectedPeripherialId diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 4ffdffca..871c54af 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -202,6 +202,8 @@ struct NodeListItem: View { } } } - .padding([.top, .bottom]) + .padding(.top, 4) + .padding(.bottom, 4) + //.padding([.top, .bottom]) } } diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 3ca281c2..e9a0f352 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -181,7 +181,7 @@ struct LoRaConfig: View { HStack { Image(systemName: "antenna.radiowaves.left.and.right") .foregroundColor(.accentColor) - Stepper("\(txPower)db Transmit Power", value: $txPower, in: 1...30, step: 1) + Stepper("\(txPower)dBm Transmit Power", value: $txPower, in: 1...30, step: 1) .padding(5) } } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 8136e865..2ce168ac 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -5,6 +5,7 @@ // Copyright (c) Garth Vander Houwen 9/4/22. // import SwiftUI +import CoreLocation struct MQTTConfig: View { @@ -23,9 +24,9 @@ struct MQTTConfig: View { @State var jsonEnabled = false @State var tlsEnabled = true @State var root = "msh" + @State var selectedTopic = "" @State var mqttConnected: Bool = false - - + @State var nearbyTopics = [String]() var body: some View { VStack { @@ -51,7 +52,7 @@ struct MQTTConfig: View { Toggle(isOn: $proxyToClientEnabled) { Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right") - Text("If both MQTT and the client proxy are enabled your mobile device will utilize an available network connection to connect to the specified MQTT server.") + Text("Utilizes the network connection on your phone to connect to MQTT.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -69,7 +70,7 @@ struct MQTTConfig: View { Toggle(isOn: $jsonEnabled) { Label("JSON Enabled", systemImage: "ellipsis.curlybraces") - Text("JSON mode is a limited, unencrypted MQTT output that can crash your node it should not be enabled unless you are locally integrating with home assistant") + Text("JSON mode is a limited, unencrypted MQTT output for locally integrating with home assistant") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -80,7 +81,7 @@ struct MQTTConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - Section(header: Text("Custom Server")) { + Section(header: Text("Server")) { HStack { Label("Address", systemImage: "server.rack") TextField("Server Address", text: $address) @@ -157,6 +158,7 @@ struct MQTTConfig: View { } .keyboardType(.default) .scrollDismissesKeyboard(.interactively) + .listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/) HStack { Label("Root Topic", systemImage: "tree") TextField("Root Topic", text: $root) @@ -164,8 +166,8 @@ struct MQTTConfig: View { .onChange(of: root, perform: { _ in let totalBytes = root.utf8.count // Only mess with the value if it is too big - if totalBytes > 14 { - let firstNBytes = Data(root.utf8.prefix(14)) + if totalBytes > 15 { + let firstNBytes = Data(root.utf8.prefix(15)) if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { // Set the shortName back to the last place where it was the right size root = maxBytesString @@ -177,9 +179,23 @@ struct MQTTConfig: View { .keyboardType(.asciiCapable) .scrollDismissesKeyboard(.interactively) .disableAutocorrection(true) - Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs") + .listRowSeparator(.hidden) + Text("The root topic to use for MQTT.") .foregroundColor(.gray) .font(.callout) + + if nearbyTopics.count > 0 { + Picker("Nearby Topics", selection: $selectedTopic ) { + ForEach(nearbyTopics, id: \.self) { nt in + Text(nt) + } + } + .pickerStyle(InlinePickerStyle()) + .listRowSeparator(.hidden) + Text("If the default region topic is too busy you can choose a more local topic.") + .foregroundColor(.gray) + .font(.callout) + } } Text("You can set uplink and downlink for each channel.") .font(.callout) @@ -249,6 +265,9 @@ struct MQTTConfig: View { if newRoot != node!.mqttConfig!.root { hasChanges = true } } } + .onChange(of: selectedTopic) { newSelectedTopic in + root = newSelectedTopic + } .onChange(of: enabled) { newEnabled in if node != nil && node?.mqttConfig != nil { if newEnabled != node!.mqttConfig!.enabled { hasChanges = true } @@ -290,6 +309,52 @@ struct MQTTConfig: View { } } func setMqttValues() { + + if #available(iOS 17.0, macOS 14.0, *) { + + nearbyTopics = [] + let geocoder = CLGeocoder() + if LocationsHandler.shared.locationsArray.count > 0 { + geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) -> Void in + if error != nil { + print("Failed to reverse geocode location") + return + } + + if let placemarks = placemarks, let placemark = placemarks.first { + + /// Country Topic unless you are US + if placemark.isoCountryCode ?? "unknown" != "US" { + let countryTopic = root + "/" + (placemark.isoCountryCode ?? "") + if !countryTopic.isEmpty { + nearbyTopics.append(countryTopic) + } + } + let stateTopic = root + "/" + (placemark.administrativeArea ?? "") + if !stateTopic.isEmpty { + nearbyTopics.append(stateTopic) + } + let countyTopic = root + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") + if !countyTopic.isEmpty { + nearbyTopics.append(countyTopic) + } + let cityTopic = root + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") + if !cityTopic.isEmpty { + nearbyTopics.append(cityTopic) + } + let neightborhoodTopic = root + "/" + (placemark.subLocality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") + if !neightborhoodTopic.isEmpty { + nearbyTopics.append(neightborhoodTopic) + } + + } + else + { + print("No Location") + } + }) + } + } self.enabled = (node?.mqttConfig?.enabled ?? false) self.proxyToClientEnabled = (node?.mqttConfig?.proxyToClientEnabled ?? false) self.address = node?.mqttConfig?.address ?? "" From 387e594d45d09faace04ba26d95b51db5493d4c8 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 13 Mar 2024 05:53:45 -0700 Subject: [PATCH 13/26] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 16 ++++++++-------- .../Views/Nodes/Helpers/NodeListItem.swift | 1 - 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index a0beff25..2bb892bb 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1572,7 +1572,7 @@ CODE_SIGN_ENTITLEMENTS = Meshtastic/Meshtastic.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 843; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -1584,7 +1584,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.3.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1606,7 +1606,7 @@ CODE_SIGN_ENTITLEMENTS = Meshtastic/Meshtastic.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 843; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -1618,7 +1618,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.3.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1728,7 +1728,7 @@ CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 841; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = GCH7VS5Y9R; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; @@ -1740,7 +1740,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.3.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1761,7 +1761,7 @@ CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 841; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = GCH7VS5Y9R; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; @@ -1773,7 +1773,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.0; + MARKETING_VERSION = 2.3.1; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 871c54af..c880d27b 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -204,6 +204,5 @@ struct NodeListItem: View { } .padding(.top, 4) .padding(.bottom, 4) - //.padding([.top, .bottom]) } } From 5d7693b40d19290247e537122872fa0ca0f1b34b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 13 Mar 2024 20:47:18 -0700 Subject: [PATCH 14/26] New mqtt root size --- .../Protobufs/meshtastic/clientonly.pb.swift | 33 ++++ .../meshtastic/module_config.pb.swift | 92 ++++++++++ Meshtastic/Protobufs/meshtastic/mqtt.pb.swift | 171 ++++++++++++++++++ .../Protobufs/meshtastic/portnums.pb.swift | 8 + Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 2 +- .../Settings/Config/Module/MQTTConfig.swift | 4 +- protobufs | 2 +- 7 files changed, 308 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift b/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift index 050c719d..64fd71c0 100644 --- a/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift @@ -94,8 +94,22 @@ struct DeviceProfile { fileprivate var _moduleConfig: LocalModuleConfig? = nil } +/// +/// A heartbeat message is sent by a node to indicate that it is still alive. +/// This is currently only needed to keep serial connections alive. +struct Heartbeat { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) extension DeviceProfile: @unchecked Sendable {} +extension Heartbeat: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -161,3 +175,22 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa return true } } + +extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Heartbeat" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Heartbeat, rhs: Heartbeat) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift index 3aee0200..b688479b 100644 --- a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift @@ -376,6 +376,43 @@ struct ModuleConfig { /// If true, we can use the connected phone / client to proxy messages to MQTT instead of a direct connection var proxyToClientEnabled: Bool = false + /// + /// If true, we will periodically report unencrypted information about our node to a map via MQTT + var mapReportingEnabled: Bool = false + + /// + /// Settings for reporting information about our node to a map via MQTT + var mapReportSettings: ModuleConfig.MapReportSettings { + get {return _mapReportSettings ?? ModuleConfig.MapReportSettings()} + set {_mapReportSettings = newValue} + } + /// Returns true if `mapReportSettings` has been explicitly set. + var hasMapReportSettings: Bool {return self._mapReportSettings != nil} + /// Clears the value of `mapReportSettings`. Subsequent reads from it will return its default value. + mutating func clearMapReportSettings() {self._mapReportSettings = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _mapReportSettings: ModuleConfig.MapReportSettings? = nil + } + + /// + /// Settings for reporting unencrypted information about our node to a map via MQTT + struct MapReportSettings { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// How often we should report our info to the map (in seconds) + var publishIntervalSecs: UInt32 = 0 + + /// + /// Bits of precision for the location sent (default of 32 is full precision). + var positionPrecision: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -1207,6 +1244,7 @@ extension RemoteHardwarePinType: @unchecked Sendable {} extension ModuleConfig: @unchecked Sendable {} extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} extension ModuleConfig.MQTTConfig: @unchecked Sendable {} +extension ModuleConfig.MapReportSettings: @unchecked Sendable {} extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} @@ -1518,6 +1556,8 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message 7: .standard(proto: "tls_enabled"), 8: .same(proto: "root"), 9: .standard(proto: "proxy_to_client_enabled"), + 10: .standard(proto: "map_reporting_enabled"), + 11: .standard(proto: "map_report_settings"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1535,12 +1575,18 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message case 7: try { try decoder.decodeSingularBoolField(value: &self.tlsEnabled) }() case 8: try { try decoder.decodeSingularStringField(value: &self.root) }() case 9: try { try decoder.decodeSingularBoolField(value: &self.proxyToClientEnabled) }() + case 10: try { try decoder.decodeSingularBoolField(value: &self.mapReportingEnabled) }() + case 11: try { try decoder.decodeSingularMessageField(value: &self._mapReportSettings) }() default: break } } } func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if self.enabled != false { try visitor.visitSingularBoolField(value: self.enabled, fieldNumber: 1) } @@ -1568,6 +1614,12 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message if self.proxyToClientEnabled != false { try visitor.visitSingularBoolField(value: self.proxyToClientEnabled, fieldNumber: 9) } + if self.mapReportingEnabled != false { + try visitor.visitSingularBoolField(value: self.mapReportingEnabled, fieldNumber: 10) + } + try { if let v = self._mapReportSettings { + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -1581,6 +1633,46 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message if lhs.tlsEnabled != rhs.tlsEnabled {return false} if lhs.root != rhs.root {return false} if lhs.proxyToClientEnabled != rhs.proxyToClientEnabled {return false} + if lhs.mapReportingEnabled != rhs.mapReportingEnabled {return false} + if lhs._mapReportSettings != rhs._mapReportSettings {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = ModuleConfig.protoMessageName + ".MapReportSettings" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "publish_interval_secs"), + 2: .standard(proto: "position_precision"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // 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.decodeSingularUInt32Field(value: &self.publishIntervalSecs) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.publishIntervalSecs != 0 { + try visitor.visitSingularUInt32Field(value: self.publishIntervalSecs, fieldNumber: 1) + } + if self.positionPrecision != 0 { + try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ModuleConfig.MapReportSettings, rhs: ModuleConfig.MapReportSettings) -> Bool { + if lhs.publishIntervalSecs != rhs.publishIntervalSecs {return false} + if lhs.positionPrecision != rhs.positionPrecision {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/mqtt.pb.swift b/Meshtastic/Protobufs/meshtastic/mqtt.pb.swift index 73fe4c30..43000bd1 100644 --- a/Meshtastic/Protobufs/meshtastic/mqtt.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mqtt.pb.swift @@ -55,8 +55,75 @@ struct ServiceEnvelope { fileprivate var _packet: MeshPacket? = nil } +/// +/// Information about a node intended to be reported unencrypted to a map using MQTT. +struct MapReport { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// A full name for this user, i.e. "Kevin Hester" + var longName: String = String() + + /// + /// A VERY short name, ideally two characters. + /// Suitable for a tiny OLED screen + var shortName: String = String() + + /// + /// Role of the node that applies specific settings for a particular use-case + var role: Config.DeviceConfig.Role = .client + + /// + /// Hardware model of the node, i.e. T-Beam, Heltec V3, etc... + var hwModel: HardwareModel = .unset + + /// + /// Device firmware version string + var firmwareVersion: String = String() + + /// + /// The region code for the radio (US, CN, EU433, etc...) + var region: Config.LoRaConfig.RegionCode = .unset + + /// + /// Modem preset used by the radio (LongFast, MediumSlow, etc...) + var modemPreset: Config.LoRaConfig.ModemPreset = .longFast + + /// + /// Whether the node has a channel with default PSK and name (LongFast, MediumSlow, etc...) + /// and it uses the default frequency slot given the region and modem preset. + var hasDefaultChannel_p: Bool = false + + /// + /// Latitude: multiply by 1e-7 to get degrees in floating point + var latitudeI: Int32 = 0 + + /// + /// Longitude: multiply by 1e-7 to get degrees in floating point + var longitudeI: Int32 = 0 + + /// + /// Altitude in meters above MSL + var altitude: Int32 = 0 + + /// + /// Indicates the bits of precision for latitude and longitude set by the sending node + var positionPrecision: UInt32 = 0 + + /// + /// Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT) + var numOnlineLocalNodes: UInt32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) extension ServiceEnvelope: @unchecked Sendable {} +extension MapReport: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -110,3 +177,107 @@ extension ServiceEnvelope: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen return true } } + +extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".MapReport" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "long_name"), + 2: .standard(proto: "short_name"), + 3: .same(proto: "role"), + 4: .standard(proto: "hw_model"), + 5: .standard(proto: "firmware_version"), + 6: .same(proto: "region"), + 7: .standard(proto: "modem_preset"), + 8: .standard(proto: "has_default_channel"), + 9: .standard(proto: "latitude_i"), + 10: .standard(proto: "longitude_i"), + 11: .same(proto: "altitude"), + 12: .standard(proto: "position_precision"), + 13: .standard(proto: "num_online_local_nodes"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // 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.longName) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.shortName) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.role) }() + case 4: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() + case 5: try { try decoder.decodeSingularStringField(value: &self.firmwareVersion) }() + case 6: try { try decoder.decodeSingularEnumField(value: &self.region) }() + case 7: try { try decoder.decodeSingularEnumField(value: &self.modemPreset) }() + case 8: try { try decoder.decodeSingularBoolField(value: &self.hasDefaultChannel_p) }() + case 9: try { try decoder.decodeSingularSFixed32Field(value: &self.latitudeI) }() + case 10: try { try decoder.decodeSingularSFixed32Field(value: &self.longitudeI) }() + case 11: try { try decoder.decodeSingularInt32Field(value: &self.altitude) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &self.numOnlineLocalNodes) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.longName.isEmpty { + try visitor.visitSingularStringField(value: self.longName, fieldNumber: 1) + } + if !self.shortName.isEmpty { + try visitor.visitSingularStringField(value: self.shortName, fieldNumber: 2) + } + if self.role != .client { + try visitor.visitSingularEnumField(value: self.role, fieldNumber: 3) + } + if self.hwModel != .unset { + try visitor.visitSingularEnumField(value: self.hwModel, fieldNumber: 4) + } + if !self.firmwareVersion.isEmpty { + try visitor.visitSingularStringField(value: self.firmwareVersion, fieldNumber: 5) + } + if self.region != .unset { + try visitor.visitSingularEnumField(value: self.region, fieldNumber: 6) + } + if self.modemPreset != .longFast { + try visitor.visitSingularEnumField(value: self.modemPreset, fieldNumber: 7) + } + if self.hasDefaultChannel_p != false { + try visitor.visitSingularBoolField(value: self.hasDefaultChannel_p, fieldNumber: 8) + } + if self.latitudeI != 0 { + try visitor.visitSingularSFixed32Field(value: self.latitudeI, fieldNumber: 9) + } + if self.longitudeI != 0 { + try visitor.visitSingularSFixed32Field(value: self.longitudeI, fieldNumber: 10) + } + if self.altitude != 0 { + try visitor.visitSingularInt32Field(value: self.altitude, fieldNumber: 11) + } + if self.positionPrecision != 0 { + try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 12) + } + if self.numOnlineLocalNodes != 0 { + try visitor.visitSingularUInt32Field(value: self.numOnlineLocalNodes, fieldNumber: 13) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: MapReport, rhs: MapReport) -> Bool { + if lhs.longName != rhs.longName {return false} + if lhs.shortName != rhs.shortName {return false} + if lhs.role != rhs.role {return false} + if lhs.hwModel != rhs.hwModel {return false} + if lhs.firmwareVersion != rhs.firmwareVersion {return false} + if lhs.region != rhs.region {return false} + if lhs.modemPreset != rhs.modemPreset {return false} + if lhs.hasDefaultChannel_p != rhs.hasDefaultChannel_p {return false} + if lhs.latitudeI != rhs.latitudeI {return false} + if lhs.longitudeI != rhs.longitudeI {return false} + if lhs.altitude != rhs.altitude {return false} + if lhs.positionPrecision != rhs.positionPrecision {return false} + if lhs.numOnlineLocalNodes != rhs.numOnlineLocalNodes {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift index 937ff635..c8948d7d 100644 --- a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift @@ -181,6 +181,10 @@ enum PortNum: SwiftProtobuf.Enum { /// Portnum for payloads from the official Meshtastic ATAK plugin case atakPlugin // = 72 + /// + /// Provides unencrypted information about a node for consumption by a map via MQTT + case mapReportApp // = 73 + /// /// Private applications should use portnums >= 256. /// To simplify initial development and testing you can use "PRIVATE_APP" @@ -226,6 +230,7 @@ enum PortNum: SwiftProtobuf.Enum { case 70: self = .tracerouteApp case 71: self = .neighborinfoApp case 72: self = .atakPlugin + case 73: self = .mapReportApp case 256: self = .privateApp case 257: self = .atakForwarder case 511: self = .max @@ -258,6 +263,7 @@ enum PortNum: SwiftProtobuf.Enum { case .tracerouteApp: return 70 case .neighborinfoApp: return 71 case .atakPlugin: return 72 + case .mapReportApp: return 73 case .privateApp: return 256 case .atakForwarder: return 257 case .max: return 511 @@ -295,6 +301,7 @@ extension PortNum: CaseIterable { .tracerouteApp, .neighborinfoApp, .atakPlugin, + .mapReportApp, .privateApp, .atakForwarder, .max, @@ -334,6 +341,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding { 70: .same(proto: "TRACEROUTE_APP"), 71: .same(proto: "NEIGHBORINFO_APP"), 72: .same(proto: "ATAK_PLUGIN"), + 73: .same(proto: "MAP_REPORT_APP"), 256: .same(proto: "PRIVATE_APP"), 257: .same(proto: "ATAK_FORWARDER"), 511: .same(proto: "MAX"), diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index e1e87b6a..38ce2e1b 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -43,7 +43,7 @@ struct DeviceMetricsLog: View { .accessibilityLabel("Line Series") .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") .foregroundStyle(batteryChartColor) - .interpolationMethod(.cardinal) + .interpolationMethod(.linear) Plot { PointMark( diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 2ce168ac..97a7225a 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -166,8 +166,8 @@ struct MQTTConfig: View { .onChange(of: root, perform: { _ in let totalBytes = root.utf8.count // Only mess with the value if it is too big - if totalBytes > 15 { - let firstNBytes = Data(root.utf8.prefix(15)) + if totalBytes > 30 { + let firstNBytes = Data(root.utf8.prefix(30)) if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { // Set the shortName back to the last place where it was the right size root = maxBytesString diff --git a/protobufs b/protobufs index 5a97acb1..7e3ee8cd 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 5a97acb17543a10e114675a205e3274a83e721af +Subproject commit 7e3ee8cd96740910d0611433cb9a05a7a692568c From d84f2ad91a63b135df9e2ff52072b0579567fafb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 14 Mar 2024 00:04:35 -0700 Subject: [PATCH 15/26] Add power telemetry config. --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Helpers/BLEManager.swift | 3 + .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 452 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 6 + .../Config/Module/TelemetryConfig.swift | 57 ++- 6 files changed, 520 insertions(+), 4 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 30.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 2bb892bb..4dac1356 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -370,6 +370,7 @@ DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = ""; }; DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = ""; }; + DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 30.xcdatamodel"; sourceTree = ""; }; DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = ""; }; DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = ""; }; @@ -1884,6 +1885,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */, DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */, DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */, D93069062B81D8900066FBC8 /* MeshtasticDataModelV 27.xcdatamodel */, @@ -1914,7 +1916,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */; + currentVersion = DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5d85d325..980e2870 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -710,12 +710,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } case .paxcounterApp: paxCounterPacket(packet: decodedInfo.packet, context: context!) + case .mapReportApp: + MeshLogger.log("🕸️ MESH PACKET received for Map Report App UNHANDLED\(try! decodedInfo.packet.jsonString())") case .UNRECOGNIZED: MeshLogger.log("🕸️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .max: print("MAX PORT NUM OF 511") case .atakPlugin: MeshLogger.log("🕸️ MESH PACKET received for ATAK Plugin App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + } if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index a647b881..e8a31698 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 29.xcdatamodel + MeshtasticDataModelV 30.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 30.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 30.xcdatamodel/contents new file mode 100644 index 00000000..733896d1 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 30.xcdatamodel/contents @@ -0,0 +1,452 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index cdd0d91e..ee6ceac7 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -1317,6 +1317,9 @@ func upsertTelemetryModuleConfigPacket(config: Meshtastic.ModuleConfig.Telemetry newTelemetryConfig.environmentMeasurementEnabled = config.environmentMeasurementEnabled newTelemetryConfig.environmentScreenEnabled = config.environmentScreenEnabled newTelemetryConfig.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit + newTelemetryConfig.powerMeasurementEnabled = config.powerMeasurementEnabled + newTelemetryConfig.powerUpdateInterval = Int32(config.powerUpdateInterval) + newTelemetryConfig.powerScreenEnabled = config.powerScreenEnabled fetchedNode[0].telemetryConfig = newTelemetryConfig } else { @@ -1325,6 +1328,9 @@ func upsertTelemetryModuleConfigPacket(config: Meshtastic.ModuleConfig.Telemetry fetchedNode[0].telemetryConfig?.environmentMeasurementEnabled = config.environmentMeasurementEnabled fetchedNode[0].telemetryConfig?.environmentScreenEnabled = config.environmentScreenEnabled fetchedNode[0].telemetryConfig?.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit + fetchedNode[0].telemetryConfig?.powerMeasurementEnabled = config.powerMeasurementEnabled + fetchedNode[0].telemetryConfig?.powerUpdateInterval = Int32(config.powerUpdateInterval) + fetchedNode[0].telemetryConfig?.powerScreenEnabled = config.powerScreenEnabled } do { diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index c90be6da..48cb9b26 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -21,6 +21,10 @@ struct TelemetryConfig: View { @State var environmentMeasurementEnabled = false @State var environmentScreenEnabled = false @State var environmentDisplayFahrenheit = false + @State var powerMeasurementEnabled = false + @State var powerUpdateInterval = 0 + @State var powerScreenEnabled = false + var body: some View { VStack { @@ -30,7 +34,9 @@ struct TelemetryConfig: View { Section(header: Text("update.interval")) { Picker("Device Metrics", selection: $deviceUpdateInterval ) { ForEach(UpdateIntervals.allCases) { ui in - Text(ui.description) + if ui.rawValue >= 900 { + Text(ui.description) + } } } .pickerStyle(DefaultPickerStyle()) @@ -41,7 +47,9 @@ struct TelemetryConfig: View { .listRowSeparator(.visible) Picker("Sensor Metrics", selection: $environmentUpdateInterval ) { ForEach(UpdateIntervals.allCases) { ui in - Text(ui.description) + if ui.rawValue >= 900 { + Text(ui.description) + } } } .pickerStyle(DefaultPickerStyle()) @@ -67,6 +75,30 @@ struct TelemetryConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } + Section(header: Text("Power Options")) { + Toggle(isOn: $powerMeasurementEnabled) { + Label("enabled", systemImage: "bolt") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + Picker("Power Metrics", selection: $powerUpdateInterval ) { + ForEach(UpdateIntervals.allCases) { ui in + if ui.rawValue >= 900 { + Text(ui.description) + } + } + } + .pickerStyle(DefaultPickerStyle()) + .listRowSeparator(.hidden) + Text("How often power metrics are sent out over the mesh. Default is 15 minutes.") + .foregroundColor(.gray) + .font(.callout) + .listRowSeparator(.visible) + Toggle(isOn: $powerScreenEnabled) { + Label("Power Screen", systemImage: "tv") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } } .disabled(self.bleManager.connectedPeripheral == nil || node?.telemetryConfig == nil) @@ -79,6 +111,9 @@ struct TelemetryConfig: View { tc.environmentMeasurementEnabled = environmentMeasurementEnabled tc.environmentScreenEnabled = environmentScreenEnabled tc.environmentDisplayFahrenheit = environmentDisplayFahrenheit + tc.powerMeasurementEnabled = powerMeasurementEnabled + tc.powerUpdateInterval = UInt32(powerUpdateInterval) + tc.powerScreenEnabled = powerScreenEnabled let adminMessageId = bleManager.saveTelemetryModuleConfig(config: tc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true @@ -132,6 +167,21 @@ struct TelemetryConfig: View { if newEnvDisplayF != node!.telemetryConfig!.environmentDisplayFahrenheit { hasChanges = true } } } + .onChange(of: powerMeasurementEnabled) { newPowerMeasurementEnabled in + if node != nil && node?.telemetryConfig != nil { + if newPowerMeasurementEnabled != node!.telemetryConfig!.powerMeasurementEnabled { hasChanges = true } + } + } + .onChange(of: powerUpdateInterval) { newPowerUpdateInterval in + if node != nil && node?.telemetryConfig != nil { + if newPowerUpdateInterval != node!.telemetryConfig!.powerUpdateInterval { hasChanges = true } + } + } + .onChange(of: powerScreenEnabled) { newPowerScreenEnabled in + if node != nil && node?.telemetryConfig != nil { + if newPowerScreenEnabled != node!.telemetryConfig!.powerScreenEnabled { hasChanges = true } + } + } } } func setTelemetryValues() { @@ -140,6 +190,9 @@ struct TelemetryConfig: View { self.environmentMeasurementEnabled = node?.telemetryConfig?.environmentMeasurementEnabled ?? false self.environmentScreenEnabled = node?.telemetryConfig?.environmentScreenEnabled ?? false self.environmentDisplayFahrenheit = node?.telemetryConfig?.environmentDisplayFahrenheit ?? false + self.powerMeasurementEnabled = node?.telemetryConfig?.powerMeasurementEnabled ?? false + self.powerUpdateInterval = Int(node?.telemetryConfig?.powerUpdateInterval ?? 0) + self.powerScreenEnabled = node?.telemetryConfig?.powerScreenEnabled ?? false self.hasChanges = false } } From 676662bb4f9a19c1e7c6d0896b9ab47d7bdec06b Mon Sep 17 00:00:00 2001 From: Wolfgang Nagele Date: Thu, 14 Mar 2024 20:37:27 +0100 Subject: [PATCH 16/26] Drop filtering of 101% --- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 269b3020..2c5fe248 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -34,18 +34,16 @@ struct DeviceMetricsLog: View { Chart { ForEach(chartData, id: \.self) { point in - if point.batteryLevel != 101 { // Filter out magic charging value - 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(.linear) + 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(.linear) Plot { PointMark( From d5f5d7036411410635e29a9483cefb12524fea8d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 14 Mar 2024 18:58:05 -0700 Subject: [PATCH 17/26] Start work to fix the broken node map view Tidy up suggested mqtt topics --- Meshtastic/Enums/LoraConfigEnums.swift | 42 +++- .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 198 +++++++++--------- .../Settings/Config/Module/MQTTConfig.swift | 20 +- .../Config/Module/TelemetryConfig.swift | 6 +- 4 files changed, 151 insertions(+), 115 deletions(-) diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index 53084e54..781661c6 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -28,7 +28,47 @@ enum RegionCodes: Int, CaseIterable, Identifiable { case my_919 = 17 case sg_923 = 18 case lora24 = 13 - + var topic: String { + switch self { + case .unset: + "UNSET" + case .us: + "US" + case .eu433: + "EU_433" + case .eu868: + "EU_868" + case .cn: + "CN" + case .jp: + "JP" + case .anz: + "ANZ" + case .kr: + "KR" + case .tw: + "TW" + case .ru: + "RU" + case .in: + "IN" + case .nz865: + "NZ_865" + case .th: + "TH" + case .ua433: + "UA_433" + case .ua868: + "UA_868" + case .my_433: + "MY_433" + case .my_919: + "MY_919" + case .sg_923: + "SG_923" + case .lora24: + "LORA_24" + } } var id: Int { self.rawValue } var description: String { switch self { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 3511cfdb..91ca6db4 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -10,7 +10,6 @@ import CoreLocation #if canImport(MapKit) import MapKit #endif -import WeatherKit @available(iOS 17.0, macOS 14.0, *) struct NodeMapSwiftUI: View { @@ -60,6 +59,15 @@ struct NodeMapSwiftUI: View { Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { /// Node Color from node.num let nodeColor = UIColor(hex: UInt32(node.num)) + /// Convex Hull + if showConvexHull { + if lineCoords.count > 0 { + let hull = lineCoords.getConvexHull() + MapPolygon(coordinates: hull) + .stroke(Color(nodeColor.darker()), lineWidth: 3) + .foregroundStyle(Color(nodeColor).opacity(0.4)) + } + } /// Route Lines if showRouteLines { let gradient = LinearGradient( @@ -73,114 +81,96 @@ struct NodeMapSwiftUI: View { MapPolyline(coordinates: lineCoords) .stroke(gradient, style: dashed) } - /// Convex Hull - if showConvexHull { - if lineCoords.count > 0 { - let hull = lineCoords.getConvexHull() - MapPolygon(coordinates: hull) - .stroke(Color(nodeColor.darker()), lineWidth: 3) - .foregroundStyle(Color(nodeColor).opacity(0.4)) - } - } - - /// Waypoint Annotations - if waypoints.count > 0 && showWaypoints { - ForEach(Array(waypoints), id: \.id) { waypoint in - Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { - LazyVStack { - CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 35) - .onTapGesture(coordinateSpace: .named("nodemap")) { location in - selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) - } - } - } - } - } + + + /// Node Annotations ForEach(positionArray, id: \.id) { position in let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) let headingDegrees = Angle.degrees(Double(position.heading)) - /// Reduced Precision Map Circle - if position.latest && 11...16 ~= position.precisionBits { - let pp = PositionPrecision(rawValue: Int(position.precisionBits)) - let radius : CLLocationDistance = pp?.precisionMeters ?? 0 - if radius > 0.0 { - MapCircle(center: position.coordinate, radius: radius) - .foregroundStyle(Color(nodeColor).opacity(0.25)) - .stroke(.white, lineWidth: 2) +// /// Reduced Precision Map Circle +// if position.latest && 11...16 ~= position.precisionBits { +// let pp = PositionPrecision(rawValue: Int(position.precisionBits)) +// let radius : CLLocationDistance = pp?.precisionMeters ?? 0 +// if radius > 0.0 { +// MapCircle(center: position.coordinate, radius: radius) +// .foregroundStyle(Color(nodeColor).opacity(0.25)) +// .stroke(.white, lineWidth: 2) +// } +// } +// Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { +// LazyVStack { +// if position.latest { +// ZStack { +// Circle() +// .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) +// .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) +// .frame(width: 50, height: 50) +// if pf.contains(.Heading) { +// Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "octagon") +// .symbolEffect(.pulse.byLayer) +// .padding(5) +// .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) +// .background(Color(nodeColor.darker())) +// .clipShape(Circle()) +// .rotationEffect(headingDegrees) +// .onTapGesture { +// selectedPosition = (selectedPosition == position ? nil : position) +// } +// .popover(item: $selectedPosition) { selection in +// PositionPopover(position: selection) +// .padding() +// .opacity(0.8) +// .presentationCompactAdaptation(.popover) +// } +// +// } else { +// Image(systemName: "flipphone") +// .symbolEffect(.pulse.byLayer) +// .padding(5) +// .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) +// .background(Color(UIColor(hex: UInt32(node.num)).darker())) +// .clipShape(Circle()) +// .onTapGesture { +// selectedPosition = (selectedPosition == position ? nil : position) +// } +// .popover(item: $selectedPosition) { selection in +// PositionPopover(position: selection) +// .padding() +// .opacity(0.8) +// .presentationCompactAdaptation(.popover) +// } +// +// } +// } +// } else { +// if showNodeHistory { +// if pf.contains(.Heading) { +// Image(systemName: "location.north.circle") +// .resizable() +// .scaledToFit() +// .foregroundStyle(Color(UIColor(hex: UInt32(node.num))).isLight() ? .black : .white) +// .background(Color(UIColor(hex: UInt32(node.num)))) +// .clipShape(Circle()) +// .rotationEffect(headingDegrees) +// .frame(width: 16, height: 16) +// +// } else { +// Circle() +// .fill(Color(UIColor(hex: UInt32(node.num)))) +// .strokeBorder(Color(UIColor(hex: UInt32(node.num))).isLight() ? .black : .white ,lineWidth: 2) +// .frame(width: 12, height: 12) +// } +// } +// } + + } - } - Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { - LazyVStack { - if position.latest { - ZStack { - Circle() - .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) - .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) - .frame(width: 50, height: 50) - if pf.contains(.Heading) { - Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "octagon") - .symbolEffect(.pulse.byLayer) - .padding(5) - .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) - .background(Color(nodeColor.darker())) - .clipShape(Circle()) - .rotationEffect(headingDegrees) - .onTapGesture { - selectedPosition = (selectedPosition == position ? nil : position) - } - .popover(item: $selectedPosition) { selection in - PositionPopover(position: selection) - .padding() - .opacity(0.8) - .presentationCompactAdaptation(.popover) - } - - } else { - Image(systemName: "flipphone") - .symbolEffect(.pulse.byLayer) - .padding(5) - .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) - .background(Color(UIColor(hex: UInt32(node.num)).darker())) - .clipShape(Circle()) - .onTapGesture { - selectedPosition = (selectedPosition == position ? nil : position) - } - .popover(item: $selectedPosition) { selection in - PositionPopover(position: selection) - .padding() - .opacity(0.8) - .presentationCompactAdaptation(.popover) - } - - } - } - } else { - if showNodeHistory { - if pf.contains(.Heading) { - Image(systemName: "location.north.circle") - .resizable() - .scaledToFit() - .foregroundStyle(Color(UIColor(hex: UInt32(node.num))).isLight() ? .black : .white) - .background(Color(UIColor(hex: UInt32(node.num)))) - .clipShape(Circle()) - .rotationEffect(headingDegrees) - .frame(width: 16, height: 16) - - } else { - Circle() - .fill(Color(UIColor(hex: UInt32(node.num)))) - .strokeBorder(Color(UIColor(hex: UInt32(node.num))).isLight() ? .black : .white ,lineWidth: 2) - .frame(width: 12, height: 12) - } - } - } - } - } - .tag(position.time) - .annotationTitles(.automatic) - .annotationSubtitles(.automatic) - } +// } +// .tag(position.time) +// .annotationTitles(.automatic) +// .annotationSubtitles(.automatic) +// } } .mapScope(mapScope) .mapStyle(mapStyle) diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 97a7225a..3d228ad3 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -26,7 +26,9 @@ struct MQTTConfig: View { @State var root = "msh" @State var selectedTopic = "" @State var mqttConnected: Bool = false + @State var defaultTopic = "msh/US" @State var nearbyTopics = [String]() + let locale = Locale.current var body: some View { VStack { @@ -315,6 +317,8 @@ struct MQTTConfig: View { nearbyTopics = [] let geocoder = CLGeocoder() if LocationsHandler.shared.locationsArray.count > 0 { + let region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))?.topic + defaultTopic = "msh/" + (region ?? "UNSET") geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) -> Void in if error != nil { print("Failed to reverse geocode location") @@ -322,27 +326,29 @@ struct MQTTConfig: View { } if let placemarks = placemarks, let placemark = placemarks.first { - + let cc = locale.region?.identifier ?? "UNK" /// Country Topic unless you are US - if placemark.isoCountryCode ?? "unknown" != "US" { - let countryTopic = root + "/" + (placemark.isoCountryCode ?? "") + if placemark.isoCountryCode ?? "unknown" != cc { + let countryTopic = defaultTopic + "/" + (placemark.isoCountryCode ?? "") if !countryTopic.isEmpty { nearbyTopics.append(countryTopic) } } - let stateTopic = root + "/" + (placemark.administrativeArea ?? "") + let stateTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") if !stateTopic.isEmpty { nearbyTopics.append(stateTopic) } - let countyTopic = root + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") + let countyTopic = defaultTopic + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") if !countyTopic.isEmpty { nearbyTopics.append(countyTopic) } - let cityTopic = root + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") + let cityTopic = defaultTopic + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") if !cityTopic.isEmpty { nearbyTopics.append(cityTopic) } - let neightborhoodTopic = root + "/" + (placemark.subLocality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") + let neightborhoodTopic = defaultTopic + "/" + (placemark.subLocality?.lowercased() + .replacingOccurrences(of: " ", with: "") + .replacingOccurrences(of: "'", with: "") ?? "") if !neightborhoodTopic.isEmpty { nearbyTopics.append(neightborhoodTopic) } diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 48cb9b26..7483118b 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -185,13 +185,13 @@ struct TelemetryConfig: View { } } func setTelemetryValues() { - self.deviceUpdateInterval = Int(node?.telemetryConfig?.deviceUpdateInterval ?? 0) - self.environmentUpdateInterval = Int(node?.telemetryConfig?.environmentUpdateInterval ?? 0) + self.deviceUpdateInterval = Int(node?.telemetryConfig?.deviceUpdateInterval ?? 900) + self.environmentUpdateInterval = Int(node?.telemetryConfig?.environmentUpdateInterval ?? 900) self.environmentMeasurementEnabled = node?.telemetryConfig?.environmentMeasurementEnabled ?? false self.environmentScreenEnabled = node?.telemetryConfig?.environmentScreenEnabled ?? false self.environmentDisplayFahrenheit = node?.telemetryConfig?.environmentDisplayFahrenheit ?? false self.powerMeasurementEnabled = node?.telemetryConfig?.powerMeasurementEnabled ?? false - self.powerUpdateInterval = Int(node?.telemetryConfig?.powerUpdateInterval ?? 0) + self.powerUpdateInterval = Int(node?.telemetryConfig?.powerUpdateInterval ?? 900) self.powerScreenEnabled = node?.telemetryConfig?.powerScreenEnabled ?? false self.hasChanges = false } From 550add228c33fc33751151d81f90a7693aaa1b93 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 14 Mar 2024 23:05:05 -0700 Subject: [PATCH 18/26] Move node map into map content view Dont change channel for node infos directed at other nodes --- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Persistence/UpdateCoreData.swift | 9 +- .../Nodes/Helpers/Map/NodeMapContent.swift | 166 ++++++++++++++++++ .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 115 +----------- 4 files changed, 177 insertions(+), 117 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/Map/NodeMapContent.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 4dac1356..6ae7e268 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -110,6 +110,7 @@ DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */; }; DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860D26F69BAE00DC5189 /* NodeMap.swift */; }; DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; }; + DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */; }; DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */; }; DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; }; DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBE296E76EF007C176F /* WaypointFormMapKit.swift */; }; @@ -360,6 +361,7 @@ DD90860A26F645B700DC5189 /* Meshtastic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Meshtastic.entitlements; sourceTree = ""; }; DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = ""; }; DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = ""; }; + DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapContent.swift; sourceTree = ""; }; DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSettingsForm.swift; sourceTree = ""; }; DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = ""; }; DD964FBE296E76EF007C176F /* WaypointFormMapKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointFormMapKit.swift; sourceTree = ""; }; @@ -748,6 +750,7 @@ DDAD49EB2AFAE82500B4425D /* Map */ = { isa = PBXGroup; children = ( + DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */, DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */, DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */, DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */, @@ -1365,6 +1368,7 @@ DD5E5205298EE33B00D21B61 /* mesh.pb.swift in Sources */, DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */, DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, + DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */, DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */, diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index ee6ceac7..121cab33 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -144,9 +144,10 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.snr = packet.rxSnr newNode.rssi = packet.rxRssi newNode.viaMqtt = packet.viaMqtt - newNode.channel = Int32(packet.channel) + if packet.to == 4294967295 || packet.to == UserDefaults.preferredPeripheralNum { + newNode.channel = Int32(packet.channel) + } if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { - newNode.channel = Int32(nodeInfoMessage.channel) newNode.hopsAway = Int32(truncatingIfNeeded: nodeInfoMessage.hopsAway) } if let newUserMessage = try? User(serializedData: packet.decoded.payload) { @@ -212,7 +213,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi fetchedNode[0].viaMqtt = packet.viaMqtt - fetchedNode[0].channel = Int32(packet.channel) + if packet.to == 4294967295 || packet.to == UserDefaults.preferredPeripheralNum { + fetchedNode[0].channel = Int32(packet.channel) + } if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { fetchedNode[0].channel = Int32(nodeInfoMessage.channel) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapContent.swift new file mode 100644 index 00000000..2c15e9de --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapContent.swift @@ -0,0 +1,166 @@ +// +// RouteLines.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 3/14/24. +// +import SwiftUI +import MapKit + +@available(iOS 17.0, macOS 14.0, *) +struct NodeMapContent: MapContent { + + @ObservedObject var node: NodeInfoEntity + @State var showUserLocation: Bool = false + @State var positions: [PositionEntity] = [] + /// Map State User Defaults + @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false + @AppStorage("meshMapShowRouteLines") private var showRouteLines = false + @AppStorage("enableMapConvexHull") private var showConvexHull = false + @AppStorage("enableMapTraffic") private var showTraffic: Bool = false + @AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false + @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid + // Map Configuration + @Namespace var mapScope + @State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) + @State var position = MapCameraPosition.automatic + @State var scene: MKLookAroundScene? + @State var isLookingAround = false + @State var isShowingAltitude = false + @State var isEditingSettings = false + @State var selectedPosition: PositionEntity? + @State var showWaypoints = false + @State var selectedWaypoint: WaypointEntity? + @State var isMeshMap = false + + //let region: MKCoordinateRegion + + + @MapContentBuilder + var nodeMap: some MapContent { + let positionArray = node.positions?.array as? [PositionEntity] ?? [] + let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in + return position.nodeCoordinate ?? LocationsHandler.DefaultLocation + }) + /// Node Color from node.num + let nodeColor = UIColor(hex: UInt32(node.num)) + + + /// Node Annotations + ForEach(positionArray, id: \.id) { position in + let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) + let headingDegrees = Angle.degrees(Double(position.heading)) + /// Reduced Precision Map Circle + if position.latest && 11...16 ~= position.precisionBits { + let pp = PositionPrecision(rawValue: Int(position.precisionBits)) + let radius : CLLocationDistance = pp?.precisionMeters ?? 0 + if radius > 0.0 { + MapCircle(center: position.coordinate, radius: radius) + .foregroundStyle(Color(nodeColor).opacity(0.25)) + .stroke(.white, lineWidth: 2) + } + } + if showConvexHull { + if lineCoords.count > 0 { + let hull = lineCoords.getConvexHull() + MapPolygon(coordinates: hull) + .stroke(Color(nodeColor.darker()), lineWidth: 3) + .foregroundStyle(Color(nodeColor).opacity(0.4)) + } + } + /// Route Lines + if showRouteLines { + let gradient = LinearGradient( + colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], + startPoint: .leading, endPoint: .trailing + ) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: lineCoords) + .stroke(gradient, style: dashed) + } + + /// Node Annotations + ForEach(positionArray, id: \.id) { position in + Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { + LazyVStack { + if position.latest { + ZStack { + Circle() + .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) + .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) + .frame(width: 50, height: 50) + if pf.contains(.Heading) { + Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "octagon") + .symbolEffect(.pulse.byLayer) + .padding(5) + .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) + .background(Color(nodeColor.darker())) + .clipShape(Circle()) + .rotationEffect(headingDegrees) + .onTapGesture { + selectedPosition = (selectedPosition == position ? nil : position) + } + .popover(item: $selectedPosition) { selection in + PositionPopover(position: selection) + .padding() + .opacity(0.8) + .presentationCompactAdaptation(.popover) + } + + } else { + Image(systemName: "flipphone") + .symbolEffect(.pulse.byLayer) + .padding(5) + .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) + .background(Color(UIColor(hex: UInt32(node.num)).darker())) + .clipShape(Circle()) + .onTapGesture { + selectedPosition = (selectedPosition == position ? nil : position) + } + .popover(item: $selectedPosition) { selection in + PositionPopover(position: selection) + .padding() + .opacity(0.8) + .presentationCompactAdaptation(.popover) + } + } + } + } else { + if showNodeHistory { + if pf.contains(.Heading) { + Image(systemName: "location.north.circle") + .resizable() + .scaledToFit() + .foregroundStyle(Color(UIColor(hex: UInt32(node.num))).isLight() ? .black : .white) + .background(Color(UIColor(hex: UInt32(node.num)))) + .clipShape(Circle()) + .rotationEffect(headingDegrees) + .frame(width: 16, height: 16) + + } else { + Circle() + .fill(Color(UIColor(hex: UInt32(node.num)))) + .strokeBorder(Color(UIColor(hex: UInt32(node.num))).isLight() ? .black : .white ,lineWidth: 2) + .frame(width: 12, height: 12) + } + } + } + } + } + .tag(position.time) + .annotationTitles(.automatic) + .annotationSubtitles(.automatic) + } + } + } + + @MapContentBuilder + var body: some MapContent { + if node.positions?.count ?? 0 > 0 { + nodeMap + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 91ca6db4..3b2b5d31 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -57,120 +57,7 @@ struct NodeMapSwiftUI: View { ZStack { MapReader { reader in Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - /// Node Color from node.num - let nodeColor = UIColor(hex: UInt32(node.num)) - /// Convex Hull - if showConvexHull { - if lineCoords.count > 0 { - let hull = lineCoords.getConvexHull() - MapPolygon(coordinates: hull) - .stroke(Color(nodeColor.darker()), lineWidth: 3) - .foregroundStyle(Color(nodeColor).opacity(0.4)) - } - } - /// Route Lines - if showRouteLines { - let gradient = LinearGradient( - colors: [Color(nodeColor.lighter().lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], - startPoint: .leading, endPoint: .trailing - ) - let dashed = StrokeStyle( - lineWidth: 3, - lineCap: .round, lineJoin: .round, dash: [10, 10] - ) - MapPolyline(coordinates: lineCoords) - .stroke(gradient, style: dashed) - } - - - - /// Node Annotations - ForEach(positionArray, id: \.id) { position in - let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) - let headingDegrees = Angle.degrees(Double(position.heading)) -// /// Reduced Precision Map Circle -// if position.latest && 11...16 ~= position.precisionBits { -// let pp = PositionPrecision(rawValue: Int(position.precisionBits)) -// let radius : CLLocationDistance = pp?.precisionMeters ?? 0 -// if radius > 0.0 { -// MapCircle(center: position.coordinate, radius: radius) -// .foregroundStyle(Color(nodeColor).opacity(0.25)) -// .stroke(.white, lineWidth: 2) -// } -// } -// Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { -// LazyVStack { -// if position.latest { -// ZStack { -// Circle() -// .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) -// .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) -// .frame(width: 50, height: 50) -// if pf.contains(.Heading) { -// Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "octagon") -// .symbolEffect(.pulse.byLayer) -// .padding(5) -// .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) -// .background(Color(nodeColor.darker())) -// .clipShape(Circle()) -// .rotationEffect(headingDegrees) -// .onTapGesture { -// selectedPosition = (selectedPosition == position ? nil : position) -// } -// .popover(item: $selectedPosition) { selection in -// PositionPopover(position: selection) -// .padding() -// .opacity(0.8) -// .presentationCompactAdaptation(.popover) -// } -// -// } else { -// Image(systemName: "flipphone") -// .symbolEffect(.pulse.byLayer) -// .padding(5) -// .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) -// .background(Color(UIColor(hex: UInt32(node.num)).darker())) -// .clipShape(Circle()) -// .onTapGesture { -// selectedPosition = (selectedPosition == position ? nil : position) -// } -// .popover(item: $selectedPosition) { selection in -// PositionPopover(position: selection) -// .padding() -// .opacity(0.8) -// .presentationCompactAdaptation(.popover) -// } -// -// } -// } -// } else { -// if showNodeHistory { -// if pf.contains(.Heading) { -// Image(systemName: "location.north.circle") -// .resizable() -// .scaledToFit() -// .foregroundStyle(Color(UIColor(hex: UInt32(node.num))).isLight() ? .black : .white) -// .background(Color(UIColor(hex: UInt32(node.num)))) -// .clipShape(Circle()) -// .rotationEffect(headingDegrees) -// .frame(width: 16, height: 16) -// -// } else { -// Circle() -// .fill(Color(UIColor(hex: UInt32(node.num)))) -// .strokeBorder(Color(UIColor(hex: UInt32(node.num))).isLight() ? .black : .white ,lineWidth: 2) -// .frame(width: 12, height: 12) -// } -// } -// } - - - } -// } -// .tag(position.time) -// .annotationTitles(.automatic) -// .annotationSubtitles(.automatic) -// } + NodeMapContent(node: node) } .mapScope(mapScope) .mapStyle(mapStyle) From 1769b63466931d5b6c26a7a924f54c3397570e44 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 14 Mar 2024 23:17:36 -0700 Subject: [PATCH 19/26] Delete extra variables, zoom out more to start on the node map --- .../Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 3b2b5d31..cf6d90e3 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -39,6 +39,8 @@ struct NodeMapSwiftUI: View { @State var selectedWaypoint: WaypointEntity? @State var isMeshMap = false + @State private var mapRegion = MKCoordinateRegion.init() + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], predicate: NSPredicate( format: "expire == nil || expire >= %@", Date() as NSDate @@ -46,12 +48,7 @@ struct NodeMapSwiftUI: View { private var waypoints: FetchedResults var body: some View { - - let positionArray = node.positions?.array as? [PositionEntity] ?? [] var mostRecent = node.positions?.lastObject as? PositionEntity - let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in - return position.nodeCoordinate ?? LocationsHandler.DefaultLocation - }) if node.hasPositions { ZStack { @@ -119,7 +116,7 @@ struct NodeMapSwiftUI: View { if node.positions?.count ?? 0 > 1 { position = .automatic } else { - position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 150, heading: 0, pitch: 60)) + position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 8000, heading: 0, pitch: 60)) } if let mostRecent { Task { @@ -143,7 +140,7 @@ struct NodeMapSwiftUI: View { if node.positions?.count ?? 0 > 1 { position = .automatic } else { - position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 5000, heading: 0, pitch: 60)) + position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 8000, heading: 0, pitch: 60)) } if self.scene == nil { Task { From 8d6aa9e88bc2a6594593ebcd7180d7cc51efd446 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 15 Mar 2024 14:29:50 -0700 Subject: [PATCH 20/26] stop device role save crash --- Meshtastic/Views/Settings/Config/DeviceConfig.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 6e190609..4ffa72b9 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -23,7 +23,7 @@ struct DeviceConfig: View { @State var serialEnabled = true @State var debugLogEnabled = false @State var rebroadcastMode = 0 - @State var nodeInfoBroadcastSecs = 900 + @State var nodeInfoBroadcastSecs = 10800 @State var doubleTapAsButtonPress = false @State var isManaged = false @@ -176,6 +176,9 @@ struct DeviceConfig: View { dc.buttonGpio = UInt32(buttonGPIO) dc.buzzerGpio = UInt32(buzzerGPIO) dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue() + if nodeInfoBroadcastSecs < 0 { + nodeInfoBroadcastSecs = 10800 + } dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs) dc.doubleTapAsButtonPress = doubleTapAsButtonPress dc.isManaged = isManaged From e4284d9741db865d0621d7ce342eadcd30657b3e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 17 Mar 2024 09:07:11 -0700 Subject: [PATCH 21/26] Zero hops, defaults for node info broadcast --- Meshtastic/Views/Settings/Config/DeviceConfig.swift | 6 +++--- Meshtastic/Views/Settings/Config/LoRaConfig.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 4ffa72b9..64e5b330 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -176,9 +176,6 @@ struct DeviceConfig: View { dc.buttonGpio = UInt32(buttonGPIO) dc.buzzerGpio = UInt32(buzzerGPIO) dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue() - if nodeInfoBroadcastSecs < 0 { - nodeInfoBroadcastSecs = 10800 - } dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs) dc.doubleTapAsButtonPress = doubleTapAsButtonPress dc.isManaged = isManaged @@ -271,6 +268,9 @@ struct DeviceConfig: View { self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0) self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0) self.nodeInfoBroadcastSecs = Int(node?.deviceConfig?.nodeInfoBroadcastSecs ?? 900) + if nodeInfoBroadcastSecs < 3600 { + nodeInfoBroadcastSecs = 3600 + } self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false self.isManaged = node?.deviceConfig?.isManaged ?? false self.hasChanges = false diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index e9a0f352..3bfe9a41 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -130,12 +130,12 @@ struct LoRaConfig: View { } VStack(alignment: .leading) { Picker("Number of hops", selection: $hopLimit) { - ForEach(1..<8) { + ForEach(0..<8) { Text("\($0)") - .tag($0 == 0 ? 3 : $0) + .tag($0) } } - Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully.") + Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. O hop broadcast messages will not get ACKs.") .foregroundColor(.gray) .font(.callout) } From 8d2b7050684cc0d184ec13579fe547123c8fcc81 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 17 Mar 2024 10:08:03 -0700 Subject: [PATCH 22/26] Split channel form out into its own view --- Meshtastic.xcodeproj/project.pbxproj | 28 +- Meshtastic/Views/Settings/Channels.swift | 234 +--------------- .../Views/Settings/Channels/ChannelForm.swift | 254 ++++++++++++++++++ 3 files changed, 276 insertions(+), 240 deletions(-) create mode 100644 Meshtastic/Views/Settings/Channels/ChannelForm.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6ae7e268..9a07d126 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -111,6 +111,7 @@ DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860D26F69BAE00DC5189 /* NodeMap.swift */; }; DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; }; DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */; }; + DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */; }; DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */; }; DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; }; DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBE296E76EF007C176F /* WaypointFormMapKit.swift */; }; @@ -362,6 +363,7 @@ DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = ""; }; DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = ""; }; DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapContent.swift; sourceTree = ""; }; + DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelForm.swift; sourceTree = ""; }; DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSettingsForm.swift; sourceTree = ""; }; DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = ""; }; DD964FBE296E76EF007C176F /* WaypointFormMapKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointFormMapKit.swift; sourceTree = ""; }; @@ -560,21 +562,6 @@ path = CoreData; sourceTree = ""; }; - DD2100802B0E676E00F2F116 /* Routes */ = { - isa = PBXGroup; - children = ( - DD2100832B0E67AD00F2F116 /* RouteMap */, - ); - path = Routes; - sourceTree = ""; - }; - DD2100832B0E67AD00F2F116 /* RouteMap */ = { - isa = PBXGroup; - children = ( - ); - path = RouteMap; - sourceTree = ""; - }; DD47E3CA26F0E50300029299 /* Nodes */ = { isa = PBXGroup; children = ( @@ -604,7 +591,7 @@ DD4A911C2708C57100501B7E /* Settings */ = { isa = PBXGroup; children = ( - DD2100802B0E676E00F2F116 /* Routes */, + DD93800C2BA74CE3008BEC06 /* Channels */, DD97E96728EFE9A00056DDA4 /* About.swift */, DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */, DD4A911D2708C65400501B7E /* AppSettings.swift */, @@ -747,6 +734,14 @@ name = Frameworks; sourceTree = ""; }; + DD93800C2BA74CE3008BEC06 /* Channels */ = { + isa = PBXGroup; + children = ( + DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */, + ); + path = Channels; + sourceTree = ""; + }; DDAD49EB2AFAE82500B4425D /* Map */ = { isa = PBXGroup; children = ( @@ -1229,6 +1224,7 @@ DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */, DD1933782B084F4200771CD5 /* Measurement.swift in Sources */, DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */, + DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */, DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */, DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */, DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */, diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index db9c9a03..a7cfebf0 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -40,18 +40,15 @@ struct Channels: View { @State private var channelRole = 0 @State private var uplink = false @State private var downlink = false - @State private var positionPrecision = 32.0 @State private var preciseLocation = true @State private var positionsEnabled = true + @State private var supportedVersion = true /// Minimum Version for granular position configuration @State var minimumVersion = "2.2.24" - var body: some View { - - let supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame VStack { List { @@ -80,7 +77,10 @@ struct Channels: View { channelName = channel.name ?? "" uplink = channel.uplinkEnabled downlink = channel.downlinkEnabled - hasChanges = false + + print("Position Precision \(channel.positionPrecision)") + //self.positionPrecision = State(initialValue: Double(self.channel.positionPrecision)) + positionPrecision = Double(channel.positionPrecision) if !supportedVersion && channelRole == 1 { positionPrecision = 32 preciseLocation = true @@ -91,21 +91,22 @@ struct Channels: View { preciseLocation = false positionsEnabled = false } else { - positionPrecision = Double(channel.positionPrecision) if positionPrecision == 32 { preciseLocation = true positionsEnabled = true } else { preciseLocation = false } - if positionPrecision == 0 { positionsEnabled = false } else { positionsEnabled = true } } + hasChanges = false isPresentingEditView = true + + }) { VStack(alignment: .leading) { HStack { @@ -138,224 +139,9 @@ struct Channels: View { .font(.largeTitle) .padding() #endif - Form { - Section(header: Text("channel details")) { - HStack { - Text("name") - Spacer() - TextField( - "Channel Name", - text: $channelName - ) - .disableAutocorrection(true) - .keyboardType(.alphabet) - .foregroundColor(Color.gray) - .onChange(of: channelName, perform: { _ in - channelName = channelName.replacing(" ", with: "") - let totalBytes = channelName.utf8.count - // Only mess with the value if it is too big - if totalBytes > 11 { - let firstNBytes = Data(channelName.utf8.prefix(11)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the channelName back to the last place where it was the right size - channelName = maxBytesString - } - } - hasChanges = true - }) - } - HStack { - Picker("Key Size", selection: $channelKeySize) { - Text("Empty").tag(0) - Text("Default").tag(-1) - Text("1 byte").tag(1) - Text("128 bit").tag(16) - Text("192 bit").tag(24) - Text("256 bit").tag(32) - } - .pickerStyle(DefaultPickerStyle()) - Spacer() - Button { - if channelKeySize == -1 { - channelKey = "AQ==" - } else { - let key = generateChannelKey(size: channelKeySize) - channelKey = key - } - } label: { - Image(systemName: "lock.rotation") - .font(.title) - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.small) - } - HStack(alignment: .center) { - Text("Key") - Spacer() - TextField( - "Key", - text: $channelKey, - axis: .vertical - ) - .padding(6) - .disableAutocorrection(true) - .keyboardType(.alphabet) - .foregroundColor(Color.gray) - .textSelection(.enabled) - .background( - RoundedRectangle(cornerRadius: 10.0) - .stroke( - hasValidKey ? - Color.clear : - Color.red - , lineWidth: 2.0) - - ) - .onChange(of: channelKey, perform: { _ in - let tempKey = Data(base64Encoded: channelKey) ?? Data() - if tempKey.count == channelKeySize || channelKeySize == -1{ - hasValidKey = true - } - else { - hasValidKey = false - } - hasChanges = true - }) - .disabled(channelKeySize <= 0) - } - HStack { - if channelRole == 1 { - Picker("Channel Role", selection: $channelRole) { - Text("Primary").tag(1) - } - .pickerStyle(.automatic) - .disabled(true) - } else { - Text("Channel Role") - Spacer() - Picker("Channel Role", selection: $channelRole) { - Text("Disabled").tag(0) - Text("Secondary").tag(2) - } - .pickerStyle(.segmented) - } - } - } - - Section(header: Text("position")) { - - VStack(alignment: .leading) { - Toggle(isOn: $positionsEnabled) { - Label(channelRole == 1 ? "Positions Enabled" : "Allow Position Requests", systemImage: positionsEnabled ? "mappin" : "mappin.slash") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .disabled(!supportedVersion) - } - - if positionsEnabled { - VStack(alignment: .leading) { - Toggle(isOn: $preciseLocation) { - Label("Precise Location", systemImage: "scope") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .disabled(!supportedVersion) - .listRowSeparator(.visible) - .onChange(of: preciseLocation) { pl in - if pl == false { - positionPrecision = 13 - } - } - } - - if !preciseLocation { - VStack(alignment: .leading) { - Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider( - value: $positionPrecision, - in: 11...16, - step: 1 - ) - { - } minimumValueLabel: { - Image(systemName: "minus") - } maximumValueLabel: { - Image(systemName: "plus") - } - Text(PositionPrecision(rawValue: Int(positionPrecision))?.description ?? "") - .foregroundColor(.gray) - .font(.callout) - } - } - } - } - Section(header: Text("mqtt")) { - Toggle(isOn: $uplink) { - Label("Uplink Enabled", systemImage: "arrowshape.up") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) - - Toggle(isOn: $downlink) { - Label("Downlink Enabled", systemImage: "arrowshape.down") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) - } - } + ChannelForm(channelIndex: $channelIndex, channelName: $channelName, channelKeySize: $channelKeySize, channelKey: $channelKey, channelRole: $channelRole, uplink: $uplink, downlink: $downlink, positionPrecision: $positionPrecision, preciseLocation: $preciseLocation, positionsEnabled: $positionsEnabled, hasChanges: $hasChanges, hasValidKey: $hasValidKey, supportedVersion: $supportedVersion) .onAppear { - let tempKey = Data(base64Encoded: channelKey) ?? Data() - if tempKey.count == channelKeySize || channelKeySize == -1 { - hasValidKey = true - } - else { - hasValidKey = false - } - } - .onChange(of: channelName) { _ in - hasChanges = true - } - .onChange(of: channelKeySize) { _ in - if channelKeySize == -1 { - channelKey = "AQ==" - } else { - let key = generateChannelKey(size: channelKeySize) - channelKey = key - } - hasChanges = true - } - .onChange(of: channelKey) { _ in - hasChanges = true - } - .onChange(of: channelRole) { _ in - hasChanges = true - } - .onChange(of: preciseLocation) { loc in - if loc { - positionPrecision = 32 - } else { - positionPrecision = 14 - } - hasChanges = true - } - .onChange(of: positionPrecision) { _ in - hasChanges = true - } - .onChange(of: positionsEnabled) { pe in - if pe { - if positionPrecision == 0 { - positionPrecision = 32 - } - } else { - positionPrecision = 0 - } - hasChanges = true - } - .onChange(of: uplink) { _ in - hasChanges = true - } - .onChange(of: downlink) { _ in - hasChanges = true + supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame } HStack { Button { diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift new file mode 100644 index 00000000..c191d345 --- /dev/null +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -0,0 +1,254 @@ +// +// ChannelForm.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 3/17/24. +// + + +import SwiftUI +#if canImport(MapKit) +import MapKit +#endif + +struct ChannelForm: View { + + @Binding var channelIndex: Int32 + @Binding var channelName: String + @Binding var channelKeySize: Int + @Binding var channelKey: String + @Binding var channelRole: Int + @Binding var uplink: Bool + @Binding var downlink: Bool + @Binding var positionPrecision: Double + @Binding var preciseLocation: Bool + @Binding var positionsEnabled: Bool + + @Binding var hasChanges: Bool + @Binding var hasValidKey: Bool + + /// Minimum Version for granular position configuration + @Binding var supportedVersion: Bool + + var body: some View { + + NavigationStack { + Form { + Section(header: Text("channel details")) { + HStack { + Text("name") + Spacer() + TextField( + "Channel Name", + text: $channelName + ) + .disableAutocorrection(true) + .keyboardType(.alphabet) + .foregroundColor(Color.gray) + .onChange(of: channelName, perform: { _ in + channelName = channelName.replacing(" ", with: "") + let totalBytes = channelName.utf8.count + // Only mess with the value if it is too big + if totalBytes > 11 { + let firstNBytes = Data(channelName.utf8.prefix(11)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the channelName back to the last place where it was the right size + channelName = maxBytesString + } + } + hasChanges = true + }) + } + HStack { + Picker("Key Size", selection: $channelKeySize) { + Text("Empty").tag(0) + Text("Default").tag(-1) + Text("1 byte").tag(1) + Text("128 bit").tag(16) + Text("192 bit").tag(24) + Text("256 bit").tag(32) + } + .pickerStyle(DefaultPickerStyle()) + Spacer() + Button { + if channelKeySize == -1 { + channelKey = "AQ==" + } else { + let key = generateChannelKey(size: channelKeySize) + channelKey = key + } + } label: { + Image(systemName: "lock.rotation") + .font(.title) + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + } + HStack(alignment: .center) { + Text("Key") + Spacer() + TextField( + "Key", + text: $channelKey, + axis: .vertical + ) + .padding(6) + .disableAutocorrection(true) + .keyboardType(.alphabet) + .foregroundColor(Color.gray) + .textSelection(.enabled) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke( + hasValidKey ? + Color.clear : + Color.red + , lineWidth: 2.0) + + ) + .onChange(of: channelKey, perform: { _ in + let tempKey = Data(base64Encoded: channelKey) ?? Data() + if tempKey.count == channelKeySize || channelKeySize == -1{ + hasValidKey = true + } + else { + hasValidKey = false + } + hasChanges = true + }) + .disabled(channelKeySize <= 0) + } + HStack { + if channelRole == 1 { + Picker("Channel Role", selection: $channelRole) { + Text("Primary").tag(1) + } + .pickerStyle(.automatic) + .disabled(true) + } else { + Text("Channel Role") + Spacer() + Picker("Channel Role", selection: $channelRole) { + Text("Disabled").tag(0) + Text("Secondary").tag(2) + } + .pickerStyle(.segmented) + } + } + } + + Section(header: Text("position")) { + + VStack(alignment: .leading) { + Toggle(isOn: $positionsEnabled) { + Label(channelRole == 1 ? "Positions Enabled" : "Allow Position Requests", systemImage: positionsEnabled ? "mappin" : "mappin.slash") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .disabled(!supportedVersion) + } + + if positionsEnabled { + VStack(alignment: .leading) { + Toggle(isOn: $preciseLocation) { + Label("Precise Location", systemImage: "scope") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .disabled(!supportedVersion) + .listRowSeparator(.visible) + .onChange(of: preciseLocation) { pl in + if pl == false { + positionPrecision = 13 + } + } + } + + if !preciseLocation { + VStack(alignment: .leading) { + Label("Approximate Location", systemImage: "location.slash.circle.fill") + Slider(value: $positionPrecision, in: 11...16, step: 1) { + } minimumValueLabel: { + Image(systemName: "minus") + } maximumValueLabel: { + Image(systemName: "plus") + } + Text(PositionPrecision(rawValue: Int(positionPrecision))?.description ?? "") + .foregroundColor(.gray) + .font(.callout) + } + } + } + } + Section(header: Text("mqtt")) { + Toggle(isOn: $uplink) { + Label("Uplink Enabled", systemImage: "arrowshape.up") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + + Toggle(isOn: $downlink) { + Label("Downlink Enabled", systemImage: "arrowshape.down") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + } + } + .onChange(of: channelName) { _ in + hasChanges = true + } + .onChange(of: channelKeySize) { _ in + if channelKeySize == -1 { + channelKey = "AQ==" + } else { + let key = generateChannelKey(size: channelKeySize) + channelKey = key + } + hasChanges = true + } + .onChange(of: channelKey) { _ in + hasChanges = true + } + .onChange(of: channelRole) { _ in + hasChanges = true + } + .onChange(of: preciseLocation) { loc in + if loc == true { + positionPrecision = 32 + } else { + positionPrecision = 14 + } + hasChanges = true + } + .onChange(of: positionPrecision) { _ in + hasChanges = true + } + .onChange(of: positionsEnabled) { pe in + if pe { + if positionPrecision == 0 { + positionPrecision = 32 + } + } else { + positionPrecision = 0 + } + hasChanges = true + } + .onChange(of: uplink) { _ in + hasChanges = true + } + .onChange(of: downlink) { _ in + hasChanges = true + } + .onAppear { + let tempKey = Data(base64Encoded: channelKey) ?? Data() + if tempKey.count == channelKeySize || channelKeySize == -1 { + hasValidKey = true + } + else { + hasValidKey = false + } + } + } + .presentationDetents([.fraction(0.45), .fraction(0.65)]) + .presentationDragIndicator(.visible) + } +} From 8528f08572e5149d74fb35cb3ddad44b2c8fa1fe Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 17 Mar 2024 10:26:42 -0700 Subject: [PATCH 23/26] Convert channels to use the selected pattern for the sheet --- Meshtastic/Views/Settings/Channels.swift | 19 ++++++------------- .../Views/Settings/Channels/ChannelForm.swift | 3 --- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index a7cfebf0..6c5fe5ed 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -31,7 +31,6 @@ struct Channels: View { @State var hasChanges = false @State var hasValidKey = true - @State private var isPresentingEditView = false @State private var isPresentingSaveConfirm: Bool = false @State private var channelIndex: Int32 = 0 @State private var channelName = "" @@ -44,6 +43,7 @@ struct Channels: View { @State private var preciseLocation = true @State private var positionsEnabled = true @State private var supportedVersion = true + @State var selectedChannel: ChannelEntity? /// Minimum Version for granular position configuration @State var minimumVersion = "2.2.24" @@ -77,9 +77,6 @@ struct Channels: View { channelName = channel.name ?? "" uplink = channel.uplinkEnabled downlink = channel.downlinkEnabled - - print("Position Precision \(channel.positionPrecision)") - //self.positionPrecision = State(initialValue: Double(self.channel.positionPrecision)) positionPrecision = Double(channel.positionPrecision) if !supportedVersion && channelRole == 1 { positionPrecision = 32 @@ -104,9 +101,7 @@ struct Channels: View { } } hasChanges = false - isPresentingEditView = true - - + selectedChannel = channel }) { VStack(alignment: .leading) { HStack { @@ -132,8 +127,7 @@ struct Channels: View { } } } - .sheet(isPresented: $isPresentingEditView) { - + .sheet(item: $selectedChannel) { selection in #if targetEnvironment(macCatalyst) Text("channel") .font(.largeTitle) @@ -208,11 +202,10 @@ struct Channels: View { let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) if adminMessageId > 0 { - self.isPresentingEditView = false + selectedChannel = nil channelName = "" channelRole = 2 hasChanges = false - //_ = bleManager.getChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) } } label: { Label("save", systemImage: "square.and.arrow.down") @@ -224,7 +217,7 @@ struct Channels: View { .padding(.bottom) #if targetEnvironment(macCatalyst) Button { - isPresentingEditView = false + goBack() } label: { Label("close", systemImage: "xmark") } @@ -256,7 +249,7 @@ struct Channels: View { uplink = false downlink = false hasChanges = true - isPresentingEditView = true + selectedChannel = ChannelEntity(context: context) } label: { Label("Add Channel", systemImage: "plus.square") diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index c191d345..7176d802 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -23,11 +23,8 @@ struct ChannelForm: View { @Binding var positionPrecision: Double @Binding var preciseLocation: Bool @Binding var positionsEnabled: Bool - @Binding var hasChanges: Bool @Binding var hasValidKey: Bool - - /// Minimum Version for granular position configuration @Binding var supportedVersion: Bool var body: some View { From c18edc5bf4f94ebd4d87020631d7c47e3438bffc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 17 Mar 2024 11:00:27 -0700 Subject: [PATCH 24/26] Add french to project file --- Meshtastic.xcodeproj/project.pbxproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 9a07d126..d4787707 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -454,6 +454,7 @@ DDDB444F29F8AC9C00EE2349 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; DDDB445129F8ACF900EE2349 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; DDDB445329F8AD1600EE2349 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; + DDDC22312BA76701002C44F1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = ""; }; DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsBundle.swift; sourceTree = ""; }; @@ -1117,6 +1118,7 @@ "zh-Hans", pl, he, + fr, ); mainGroup = DDC2E14B26CE248E0042C5E4; packageReferences = ( @@ -1435,6 +1437,7 @@ A65FA974296876BF00A97686 /* zh-Hans */, DDF6B24B2A9C2FC800BA6931 /* pl */, DD31EC492B7F18B7006A3995 /* he */, + DDDC22312BA76701002C44F1 /* fr */, ); name = Localizable.strings; sourceTree = ""; From 710073e7b2d48f63272afa6d82afac85a52fcde2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 17 Mar 2024 11:10:43 -0700 Subject: [PATCH 25/26] Add TW --- Meshtastic.xcodeproj/project.pbxproj | 3 + zh-Hant-TW.lproj/Localizable.strings | 333 +++++++++++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 zh-Hant-TW.lproj/Localizable.strings diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index d4787707..87b0ed30 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -455,6 +455,7 @@ DDDB445129F8ACF900EE2349 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; DDDB445329F8AD1600EE2349 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; DDDC22312BA76701002C44F1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + DDDC22322BA76961002C44F1 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = ""; }; DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = ""; }; DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsBundle.swift; sourceTree = ""; }; @@ -1119,6 +1120,7 @@ pl, he, fr, + "zh-Hant-TW", ); mainGroup = DDC2E14B26CE248E0042C5E4; packageReferences = ( @@ -1438,6 +1440,7 @@ DDF6B24B2A9C2FC800BA6931 /* pl */, DD31EC492B7F18B7006A3995 /* he */, DDDC22312BA76701002C44F1 /* fr */, + DDDC22322BA76961002C44F1 /* zh-Hant-TW */, ); name = Localizable.strings; sourceTree = ""; diff --git a/zh-Hant-TW.lproj/Localizable.strings b/zh-Hant-TW.lproj/Localizable.strings new file mode 100644 index 00000000..6f36c512 --- /dev/null +++ b/zh-Hant-TW.lproj/Localizable.strings @@ -0,0 +1,333 @@ +/* + Localizable.strings + Meshtastic + + Created by BM6HIP on 2024/3/2 + +*/ +"about"="關於"; +"about.meshtastic"="關於 Meshtastic"; +"admin"="管理員"; +"admin.log"="管理員消息紀錄檔"; +"ago"="ago"; +"airtime"="廣播時間"; +"always.on"="常亮"; +"ambient.lighting"="Ambient Lighting"; +"ambient.lighting.config"="Ambient Lighting Config"; +"appsettings"="設定"; +"appsettings.provide.location"="提供定位到 Mesh 網路"; +"appsettings.smartposition"="Smart Position"; +"are.you.sure"="是否確定?"; +"ascii.capable"="ASCII Capable"; +"available.radios"="可以連接的設備"; +"automatic.detection"="自動識別"; +"battery.level"="電池電量"; +"ble.name"="藍芽名稱"; +"ble.connection.timeout %d %@"="嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該電台。"; +"ble.errorcode.6 %@"="%@ 如果在首選電台的旁邊,App 將會自動重連。"; +"ble.errorcode.14 %@"="%@ 這個錯誤通常無法自動修復,你需要在系統設定的藍芽選項中忽略該電台並重新配對。"; +"ble.errorcode.pin %@"="%@ 請再次嘗試連接並仔細檢查 PIN 碼。"; +"bluetooth"="藍芽"; +"bluetooth.off"="藍芽已關閉"; +"bluetooth.config"="藍芽設置"; +"bluetooth.mode.randompin"="隨機 PIN 碼"; +"bluetooth.mode.fixedpin"="固定 PIN 碼"; +"bluetooth.mode.nopin"="不使用 PIN 碼(直接配對)"; +"bluetooth.pairingmode"="配對模式"; +"bluetooth.pin.validation"="藍芽 PIN 碼必須是 6 位數字。"; +"bytes"="字節"; +"cancel"="取消"; +"canned.messages"="通知"; +"canned.messages.config"="通知設定"; +"canned.messages.preset.manual"="手動設定"; +"canned.messages.preset.rakrotary"="RAK 旋轉編碼器"; +"canned.messages.preset.cardkb"="M5Stack 卡片鍵盤 / RAK 鍵盤"; +"channel"="頻道"; +"channel.role.disabled"="關閉"; +"channel.role.primary"="主要"; +"channel.role.secondary"="次要"; +"channel.utilization"="頻道利用率"; +"channels"="頻道"; +"clear.app.data"="清除 App 資料"; +"clear.log"="清除紀錄檔"; +"close"="關閉"; +"config.power.settings"="電源"; +"config.power.title"="電源設定"; +"config.power.section.battery"="電池"; +"config.power.section.sleep"="休眠"; +"config.power.adc.override"="ADC Override"; +"config.power.adc.multiplier"="Multiplier"; +"config.power.ls.secs"="Light Sleep Interval"; +"config.power.min.wake.secs"="最小的喚醒間隔時間"; +"config.power.saving"="省電模式"; +"config.power.saving.description"="Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button."; +"config.power.shutdown.on.power.loss"="失去電源後關機"; +"config.power.shutdown.after.secs"="之後"; +"config.power.wait.bluetooth.secs"="等待藍芽"; +"config.ringtone"="RTTTL Ringtone"; +"config.ringtone.title"="鈴聲"; +"config.ringtone.label"="Ringtone Transfer Language"; +"config.ringtone.description"="Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications."; +"config.module.paxcounter.settings"="PAX Counter"; +"config.module.paxcounter.title"="PAX Counter Config"; +"config.module.paxcounter.enabled.description"="When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be enabled for PAX counter to work."; +"config.module.paxcounter.updateinterval"="Update Interval"; +"config.module.paxcounter.updateinterval.description"="How often we can send a message to the mesh when people are detected."; +"config.save.confirm"="電台將會在設定儲存後重啟。"; +"connected.radio"="已連接的電台"; +"communicating"="與電台進行通訊中..."; +"connected"="已連接"; +"connecting"="連接中..."; +"contacts"="聯絡人"; +"contacts %@"="聯絡人 (%@)"; +"copy"="複製"; +"current"="目前"; +"default"="預設"; +"delete"="刪除"; +"detection.sensor"="檢測感測器"; +"device"="設備"; +"device.config"="電台設定"; +"device.configuration"="設備設定"; +"device.metrics.delete"="刪除所有電台指標??"; +"device.metrics.log"="電台指標紀錄檔"; +"device.role.client"="標準模式 - App 可以連接到電台進行收發操作,並且會自動轉發 Mesh 網路中其他中繼點的消息。"; +"device.role.clientmute"="靜音模式 - 與標準模式類似,App 可以連接到電台進行收發操作,但不會轉發 Mesh 網路中其他中繼點的消息。"; +"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption."; +"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\""; +"device.role.router"="纯路由模式 - 自動轉發 Mesh 網路中其他中繼點的消息,中繼模式下螢幕會熄滅,Wi-Fi 和藍芽將會進入睡眠模式,App 將無法連接到電台進行收發操作。"; +"device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼點的消息,App 也可以連接到電台進行收發操作。"; +"device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。"; +"device.role.tracker"="追蹤模式 - 用於作為 GPS 追蹤器。從該設備發送的定位數據包優先級較高,每兩分鐘廣播一次。智能位置廣播預設為關閉。"; +"direct.messages"="聊天"; +"dismiss.keyboard"="隱藏鍵盤"; +"display"="螢幕(電台螢幕)"; +"display.config"="螢幕設定"; +"distance"="距離"; +"disconnect"="斷開連接"; +"echo"="echo"; +"email.address"="電子信箱"; +"enabled"="啟用"; +"encrypted"="加密"; +"external.notification"="外部通知"; +"external.notification.config"="外部通知設定"; +"finish"="完成"; +"firmware.version"="韌體版本"; +"firmware.version.unsupported"="檢測到不支援的韌體版本,無法連接到電台。"; +"gas"="Gas"; +"gas.resistance"="Gas Resistance"; +"generate.qr.code"="生成QRcode"; +"gpsformat.dec"="十進制"; +"gpsformat.dms"="度分秒"; +"gpsformat.utm"="通用橫軸墨卡托投影"; +"gpsformat.mgrs"="軍事網格系統"; +"gpsformat.olc"="開放的位置代碼(又稱加碼)"; +"gpsformat.osgr"="英國國土測量局網格"; +"heard"="收到"; +"heard.last"="最後收到"; +"hybrid"="混合"; +"hybrid.flyover"="混合視圖"; +"include"="包含"; +"inputevent.none"="無"; +"inputevent.up"="上"; +"inputevent.down"="下"; +"inputevent.left"="左"; +"inputevent.right"="右"; +"inputevent.select"="選擇"; +"inputevent.back"="返回"; +"inputevent.cancel"="取消"; +"interval.one.second"="一秒"; +"interval.two.seconds"="兩秒"; +"interval.three.seconds"="三秒"; +"interval.four.seconds"="四秒"; +"interval.five.seconds"="五秒"; +"interval.ten.seconds"="十秒"; +"interval.fifteen.seconds"="十五秒"; +"interval.twenty.seconds"="二十秒"; +"interval.twentyfive.seconds"="二十五秒"; +"interval.thirty.seconds"="三十秒"; +"interval.fortyfive.seconds"="四十五秒"; +"interval.one.minute"="一分鐘"; +"interval.two.minutes"="兩分鐘"; +"interval.five.minutes"="五分鐘"; +"interval.ten.minutes"="十分鐘"; +"interval.fifteen.minutes"="十五分鐘"; +"interval.thirty.minutes"="三十分鐘"; +"interval.one.hour"="一小時"; +"interval.two.hours"="兩小時"; +"interval.three.hours"="三小時"; +"interval.four.hours"="四小時"; +"interval.five.hours"="五小時"; +"interval.six.hours"="六小時"; +"interval.twelve.hours"="十二小時"; +"interval.eighteen.hours"="十八小時"; +"interval.twentyfour.hours"="二十四小時"; +"interval.thirtysix.hours"="三十六小時"; +"interval.tyeight.hours"="四十八小时小時"; +"interval.eventytwo.hours"="七十二小時"; +"keyboard.type"="鍵盤類型"; +"logging"="加載中"; +"lora"="LoRa"; +"lora.config"="LoRa 設定"; +"map"="Mesh 地圖"; +"map.centering"="居中"; +"map.tiles.delete"="刪除已緩存的地圖區塊"; +"map.recentering"="自動重新居中"; +"map.use.legacy"="Use Legacy Mesh Map"; +"map.type"="地圖類型"; +"map.usertrackingmode"="使用者跟隨模式"; +"map.usertrackingmode.none"="無"; +"map.usertrackingmode.follow"="跟隨"; +"map.usertrackingmode.followwithheading"="Follow with heading"; +"mesh.live.activity"="Mesh 即時活動"; +"mesh.log"="Mesh 紀錄檔"; +"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; +"mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; +"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; +"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@"; +"mesh.log.cannedmessages.messages.received %@"="Canned Messages Messages Received For: %@"; +"mesh.log.channel.sent %@ %d"="Sent a Channel for: %@ Channel Index %d"; +"mesh.log.channel.received %d %@"="Channel %d received from: %@"; +"mesh.log.device.config %@"="收到裝置設定: %@"; +"mesh.log.display.config %@"="收到顯示模組設定: %@"; +"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@"; +"mesh.log.device.metadata.received %@"="Device Metadata admin message received from: %@"; +"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@"; +"mesh.log.externalnotification.config %@"="External Notification module config received: %@"; +"mesh.log.lora.config %@"="收到LoRa設定: %@"; +"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@"; +"mesh.log.mqtt.config %@"="MQTT module config received: %@"; +"mesh.log.myinfo %@"="MyInfo received: %@"; +"mesh.log.network.config %@"="收到網路設定: %@"; +"mesh.log.nodeinfo.received %@"="收到中繼點訊息: %@"; +"mesh.log.paxcounter %@"="PAX Counter message received for: %@"; +"mesh.log.position.config %@"="Positon config received: %@"; +"mesh.log.position.received %@"="從中繼點接收到定位封包: %@"; +"mesh.log.rangetest.config %@"="收到拉距測試模組設定: %@"; +"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; +"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; +"mesh.log.serial.config %@"="Serial module config received: %@"; +"mesh.log.sharelocation %@"="傳送iOS裝置的GPS定位封包到中繼點上: %@"; +"mesh.log.storeforward.config %@"="Store & Forward module config received: %@"; +"mesh.log.telemetry.config %@"="收到遠測模組設定: %@"; +"mesh.log.telemetry.received %@"="收到遠測資料: %@"; +"mesh.log.textmessage.received"="Message received from the text message app."; +"mesh.log.textmessage.send.failed %@"="訊息傳送失敗, 沒有正確連接到 %@"; +"mesh.log.textmessage.sent %@ %@ %@"="傳送訊息 %@ 從 %@ 到 %@"; +"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly."; +"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@"; +"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; +"mesh.log.wantconfig %@"="Issuing Want Config to %@"; +"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; +"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; +"message"="訊息"; +"message.details"="詳細訊息"; +"messages"="訊息"; +"mode"="模式"; +"module.configuration"="模塊設定"; +"mqtt"="MQTT"; +"mqtt.connect"="Connect to MQTT"; +"mqtt.config"="MQTT 設定"; +"mqtt.clientproxy"="MQTT 客户端代理"; +"mqtt.disconnect"="Disconnect from MQTT"; +"mqtt.username"="用戶名稱"; +"name"="名稱"; +"network"="網路"; +"network.config"="網路設定"; +"nodes"="中繼點"; +"nodes %@"="中繼點 (%@)"; +"no.nodes"="未找到 Meshtastic 中繼點"; +"not.connected"="未連接到電台"; +"numbers.punctuation"="數字和標點符號"; +"off"="關閉"; +"offline"="離線"; +"on.boot"="只在啟動時"; +"options"="選項"; +"password"="密碼"; +"pause"="暫停"; +"phone.gps"="手機 GPS"; +"phone.gps.interval.description"="電台通過手機獲得定位的時間間隔,但是向 Mesh 網路中更新定位的時間間隔由電台控制。"; +"position"="定位"; +"position.config"="定位設定"; +"preferred.radio"="首選電台"; +"radio.configuration"="電台設定"; +"range.test"="拉距測試"; +"range.test.blocked"="區塊範圍測試"; +"range.test.config"="拉距測試設定"; +"reply"="回復"; +"reboot"="重新啟動"; +"reboot.node"="重啟中繼點"; +"received.ack"="收到確認"; +"received.ack.real"="收件人確認"; +"resume"="恢復"; +"ringtone"="鈴聲"; +"ringtone.config"="鈴聲設定"; +"route.recorder"="路線錄製"; +"routes"="路線"; +"routing.acknowledged"="確認"; +"routing.noroute"="找不到目標"; +"routing.gotnak"="收到否認"; +"routing.timeout"="逾時"; +"routing.nointerface"="無連接"; +"routing.maxretransmit"="已達到最大重試次數"; +"routing.nochannel"="没有頻道"; +"routing.toolarge"="數據包過大"; +"routing.noresponse"="無回應"; +"routing.dutycyclelimit"="已達到物錢區域循環週期發射上限"; +"routing.badRequest"="錯誤請求"; +"routing.notauthorized"="未授權"; +"satellite"="衛星"; +"satellite.flyover"="衛星識圖"; +"save"="儲存"; +"save.config %@"="儲存%@的設定"; +"serial"="串口"; +"serial.config"="串口設定"; +"serial.mode.default"="預設"; +"serial.mode.simple"="簡單"; +"serial.mode.proto"="Protobufs"; +"serial.mode.txtmsg"="文本訊息"; +"serial.mode.nmea"="NMEA 位置"; +"settings"="設定"; +"share.channels"="分享頻道QRcode"; +"share.position"="分享位置"; +"subscribed"="連接到 Mesh 網路"; +"select.contact"="選擇聯絡人"; +"select.node"="選擇中繼點"; +"select.menu.item"="從菜單選擇項目"; +"set.region"="設定 LoRa 區域"; +"standard"="標準"; +"standard.muted"="標準靜音"; +"start"="開始"; +"ssid"="SSID"; +"storeforward"="儲存 & 轉發"; +"storeforward.config"="儲存 & 轉發設定"; +"storeforward.heartbeat"="發送心跳包"; +"tapback"="響應"; +"tapback.heart"="心"; +"tapback.thumbsup"="豎大拇指"; +"tapback.thumbsdown"="倒大拇指"; +"tapback.haha"="哈哈"; +"tapback.exclamation"="驚嘆號"; +"tapback.question"="問號"; +"tapback.poop"="便便"; +"telemetry"="遠測(傳感器)"; +"telemetry.config"="遠側設定"; +"timeout"="超時"; +"timestamp"="時間戳記"; +"tip.bluetooth.connect.title"="連接到 LoRa 電台"; +"tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 電台的信息。您可以向左滑動斷開電台,長按查看統計訊息或開始即時活動。"; +"tip.channels.create.title"="管理頻道"; +"tip.channels.create.message"="現在 Mesh 上的資料會通過主通道發送。您可以設定輔助通道來建立由自己的金鑰保護的其他訊息組 [頻道設定提示](https://meshtastic.org/docs/configuration/radio/channels/)"; +"tip.channels.share.title"="共享 Meshtastic 頻道"; +"tip.channels.share.message"="在 Meshtastic 網路中最多有 8 個頻道。第一個頻道是主頻道,大多數活動都發生在這裡,也是必需的。如果您不共享主頻道,您的第一個共享頻道就會成為其他網路的主頻道。它會在其主頻道和您的輔助頻道上對話。名稱為 admin 的頻道可遠端控制中繼點。其他頻道用於私人群组,每個群組都有自己的密鑰。"; +"tip.messages.title"="消息"; +"tip.messages.message"="您可以發送和接收1對1聊天和群聊。在任何訊息中,您都可以長按查看可用的操作,如複製、回復、拍一拍、刪除以及詳情。"; +"twitter"="Twitter"; +"unknown"="未知"; +"unknown.age"="未知時間"; +"unset"="未設置"; +"update.firmware"="更新韌體"; +"update.interval"="更新間隔"; +"user"="使用者"; +"user.details"="使用者資料"; +"voltage"="電壓"; +"waiting"="等待中..."; From 61c7ae4f8f6020139c5438e0ca211175b2366af3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 17 Mar 2024 11:13:50 -0700 Subject: [PATCH 26/26] Heartbeat PB --- .../Protobufs/meshtastic/clientonly.pb.swift | 33 --------- .../Protobufs/meshtastic/deviceonly.pb.swift | 1 + Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 68 +++++++++++++++++++ protobufs | 2 +- 4 files changed, 70 insertions(+), 34 deletions(-) diff --git a/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift b/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift index 64fd71c0..050c719d 100644 --- a/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift @@ -94,22 +94,8 @@ struct DeviceProfile { fileprivate var _moduleConfig: LocalModuleConfig? = nil } -/// -/// A heartbeat message is sent by a node to indicate that it is still alive. -/// This is currently only needed to keep serial connections alive. -struct Heartbeat { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - #if swift(>=5.5) && canImport(_Concurrency) extension DeviceProfile: @unchecked Sendable {} -extension Heartbeat: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -175,22 +161,3 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa return true } } - -extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Heartbeat" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } - } - - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Heartbeat, rhs: Heartbeat) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift index 048c99aa..950c577e 100644 --- a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift @@ -140,6 +140,7 @@ struct DeviceState { /// /// Used only during development. /// Indicates developer is testing and changes should never be saved to flash. + /// Deprecated in 2.3.1 var noSave: Bool { get {return _storage._noSave} set {_uniqueStorage()._noSave = newValue} diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index bdc8da97..2890db6c 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -2388,6 +2388,16 @@ struct ToRadio { set {payloadVariant = .mqttClientProxyMessage(newValue)} } + /// + /// Heartbeat message (used to keep the device connection awake on serial) + var heartbeat: Heartbeat { + get { + if case .heartbeat(let v)? = payloadVariant {return v} + return Heartbeat() + } + set {payloadVariant = .heartbeat(newValue)} + } + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -2415,6 +2425,9 @@ struct ToRadio { /// /// MQTT Client Proxy Message (for client / phone subscribed to MQTT sending to device) case mqttClientProxyMessage(MqttClientProxyMessage) + /// + /// Heartbeat message (used to keep the device connection awake on serial) + case heartbeat(Heartbeat) #if !swift(>=4.1) static func ==(lhs: ToRadio.OneOf_PayloadVariant, rhs: ToRadio.OneOf_PayloadVariant) -> Bool { @@ -2442,6 +2455,10 @@ struct ToRadio { guard case .mqttClientProxyMessage(let l) = lhs, case .mqttClientProxyMessage(let r) = rhs else { preconditionFailure() } return l == r }() + case (.heartbeat, .heartbeat): return { + guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -2581,6 +2598,19 @@ struct DeviceMetadata { init() {} } +/// +/// A heartbeat message is sent to the node from the client to keep the connection alive. +/// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. +struct Heartbeat { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) extension HardwareModel: @unchecked Sendable {} extension Constants: @unchecked Sendable {} @@ -2614,6 +2644,7 @@ extension Compressed: @unchecked Sendable {} extension NeighborInfo: @unchecked Sendable {} extension Neighbor: @unchecked Sendable {} extension DeviceMetadata: @unchecked Sendable {} +extension Heartbeat: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -4198,6 +4229,7 @@ extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa 4: .same(proto: "disconnect"), 5: .same(proto: "xmodemPacket"), 6: .same(proto: "mqttClientProxyMessage"), + 7: .same(proto: "heartbeat"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -4261,6 +4293,19 @@ extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa self.payloadVariant = .mqttClientProxyMessage(v) } }() + case 7: try { + var v: Heartbeat? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .heartbeat(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .heartbeat(v) + } + }() default: break } } @@ -4292,6 +4337,10 @@ extension ToRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBa guard case .mqttClientProxyMessage(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 6) }() + case .heartbeat?: try { + guard case .heartbeat(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -4527,3 +4576,22 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement return true } } + +extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Heartbeat" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Heartbeat, rhs: Heartbeat) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/protobufs b/protobufs index 7e3ee8cd..556e49ba 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 7e3ee8cd96740910d0611433cb9a05a7a692568c +Subproject commit 556e49ba619e2f4d8fa3c2dee2a94129a43d5f08