ISS tracker

De Wiki LOGre
Aller à la navigation Aller à la recherche

Projet réalisé par Fma38.

Abouti

ISS tracker 0.png Iss tracker final.jpg

Présentation

L'idée n'est pas nouvelle, et cette forme non plus. Je suis récemment tombé sur ce projet de lampe dans un globe terrestre qui fait également office de tracker ISS, via un pointeur laser. J'aime beaucoup l'idée, et mon projet reprend une grande partie de celui-ci.

La différence, c'est qu'au lieu de faire tourner le laser pour changer la longitude (axe yaw), ici c'est le globe qui tourne ! Il y a 2 avantages : d'une part le point lumineux reste visible en permanence, à l'avant ; d'autre part le globe peut tourner à l'infini, sans être obligé de revenir en arrière, puisqu'il n'y a pas de souci de câble qui s'enroule.

Mécanique

Le globe

J'ai trouvé chez Nature et Découvertes le globe terrestre parfait : il s'agit d'un petit (24cm) globe motorisé, donc déjà prévu pour tourner. Certes, la motorisation n'a pas d'intérêt, mais du coup, il y a tout ce qu'il faut pour pouvoir la refaire très simplement. Il suffit de couper les entretoises servant de support à la motorisation initiale.


Globe-terrestre-rotatif-52150100 2.jpg Globe-terrestre-rotatif-52150100 7.jpg


Axe yaw

C'est l'axe qui gère la longitude. Il est basé sur un petit moteur pas à pas Nema 8. Un petit pignon imprimé en 3D entraîne une roue plus grande, elle-même solidaire du globe.


ISS tracker 1.png


Le guidage en rotation du globe est réalisé par un tube en alu de diamètre 16mm extérieur et épaisseur 1mm, tournant dans une bague imprimée (en PLA pour l'instant, sans doute refaite en filament IGUS Iglidur par la suite). Cette bague est solidaire du globe, et insérée en force dans le tube à sa base.

L'entraînement de la sphère est réalisée par une roue dentée, par l'intermédiaire de 2 petits pions diamétralement opposés.

ISS tracker 4.png

La roue dentée comporte une lumière pour le capteur optique de homing.

Le moteur pas à pas chauffant pas mal, les 3 pièces en contact avec celui-ci doivent être réalisées en ABS.

Axe pitch

C'est l'axe qui gère la latitude. Ici, un simple servo de modélisme suffit : la station ISS est en effet sur une inclinaison de 51,65°, donc sa latitude varie entre -52° et +52 environ.

ISS tracker 2.png


J'utilise un Goteck GS-9025MG, initialement acheté pour piloter les volets d'un drone sphérique (qui fut un epic fail, mais très fun, hein Xav' ?).

Base

En guise de pieds, j'ai imprimé un morceau de la station ISS !


ISS tracker 3.png

Électronique

Le pilotage est confié à un ESP32, un XIAO ESP32C3, afin de pouvoir faire tourner Micropython.

L'alimentation un transfo 12V externe, dont le câble passe dans le tube de la rotation yaw. Un petit LM7805 régule le 5V pour la partie logique.

Axe yaw

Le moteur pas à pas est piloté par un module type StepStick à base de Trinamic 2100 : ce chip permet un pilotage très silencieux, et fonctionne bien à très basse vitesse, comme c'est le cas ici, contrairement aux chips plus standards utilisés sur les imprimantes 3D.

Un capteur optique sert de homing, pour initialiser la longitude. Une autre solution est de faire faire cette initialisation par l'utilisateur à chaque allumage : on positionne le laser à la latitude de Greenwich, on le fait clignoter pour attirer l'attention, et l'utilisateur a 30s pour faire tourner le globe jusqu'à ce que le laser tombe sur Greenwich. Passé ce délai, on considère qu'on est à la longitude zéro, et on commence le tracking.

Axe pitch

Rien de spécial à ce niveau, le servo est piloté directement par l'ESP32.

Pointeur

Idéalement, un petit laser, sinon une simple LED haute luminosité.

Logiciel

Gestion des coordonnées

Concernant la partie gestion des coordonnées, j'ai repris le code du projet d'Antoine Seveau, sur Instructables.

Les coordonnées sont récupérables via ce site. Cela retourne des données JSON sous la forme :

{
  "iss_position": {
    "longitude": "-69.7698",
    "latitude": "-20.0456"
   },
   "message": "success",
   "timestamp": 1576659474
}

Code

Cliquer sur le bouton à droite pour faire apparaître/disparaître le code...

# -*- coding: utf-8 -*-

""" ISS tracker

(C) 2023 Frederic Mantegazza

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
or see:

    http://www.gnu.org/licenses/gpl.html
"""

import time
import network
import requests
import json

import machine

# Network settings
SSID = "AndroidAP9171"
PASSWD = "dcd8016c6bf9"
URL = "http://api.open-notify.org/iss-now.json"

# ISS
ISS_LATITUDE_MIN = -51.62  # °
ISS_LATITUDE_MAX =  51.62  # °

# Longitude axis
MOTOR_STEP_PIN = 10
MOTOR_DIR_PIN = 9
MOTOR_EN_PIN = 8
MOTOR_HOME_PIN = 20
MOTOR_PULSE_LENGTH = 10    # µs
MOTOR_PULSE_PERIOD =  2    # ms
MOTOR_MICRO_STEPPING = 16
GEAR_RATIO = 38 / 10
NB_STEPS_PER_DEGREE = 200 * MOTOR_MICRO_STEPPING * GEAR_RATIO / 360
MOTOR_ACCURACY = 1.2 / NB_STEPS_PER_DEGREE / 2                       # +- 120% of half motor resolution
MOTOR_CW_DIR = 1
MOTOR_CCW_DIR = 0

# Latitude axis
SERVO_PIN = 2
SERVO_LATITUDE_MIN = -90  # °
SERVO_LATITUDE_MAX =  90  # °
SERVO_PULSE_MIN = 2400    # µs
SERVO_PULSE_MAX =  600    # µs
SERVO_PWM_FREQ =   50     # Hz

# Pointer
POINTER_PIN = 3

# Homing
# HOMING = 'auto'
HOMING = 'manual'
AUTO_HOMING_LONGITUDE = 123.4567  # °
GREENWICH_LATITUDE = 51.47889     # °
MANUAL_HOMING_DELAY = 15          # s
MANUAL_HOMING_BLINK_FREQ = 2      # s

# Misc
REFRESH_DELAY = 30  # s
TIMEOUT = 60        # s


class Wifi:
    """
    """
    def __init__(self, ssid, passwd):
        """
        """
        self._station = network.WLAN(network.STA_IF)
        self._station.active(True)


    def scan(self):
        """
        """
        print("Scanning for WiFi networks, please wait...")
        for ssid, bssid, channel, RSSI, authmode, hidden in self._station.scan():
            print("* {:s}".format(ssid))
            print("   - Channel: {}".format(channel))
            print("   - RSSI: {}".format(RSSI))
            print("   - BSSID: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}".format(*bssid))
            print()

    def connect(self):
        """
        """
        print("\nEstablishing connection... ", end='')
        
        while not self._station.isconnected():
            print("\nRetry... ", end='')
            self._station.connect(SSID, PASSWD)
            time.sleep(3)

        print("done.")
        print("My IP Address:", self._station.ifconfig()[0])


class ISS:
    """
    """
    def __init__(self, url):
        """
        """
        self._url = url

    def getPosition(self):
        """
        """
                   
        # Perform HTTP GET request on a non-SSL web
        response = requests.get(self._url)
        
        # Check if the request was successful
        if response.status_code == 200:
            
            # Parse the JSON response
            data = json.loads(response.text)
#             print(data)
            dateAsTuple = time.localtime(int(data['timestamp']))
            date = "{0:04d}-{1:02d}-{2:02d}, {3:02d}:{4:02d}:{5:02d}".format(*dateAsTuple)
            longitude = float(data['iss_position']['longitude'])
            latitude  = float(data['iss_position']['latitude'])
            print(f"\nISS position (at {date}): {longitude: 08.4f}° / {latitude: 07.4f}°")
                               
        else:
            raise RuntimeError("Failed to get ISS position!")
            
        return (date, longitude, latitude)


class LongitudeAxis:
    """
    """
    def __init__(self):
        """
        """
        self._stepPin = machine.Pin(MOTOR_STEP_PIN, machine.Pin.OUT)
        self._dirPin = machine.Pin(MOTOR_DIR_PIN, machine.Pin.OUT)
        self._enablePin = machine.Pin(MOTOR_EN_PIN, machine.Pin.OUT)
        self._homePin = machine.Pin(MOTOR_HOME_PIN, machine.Pin.IN, machine.Pin.PULL_UP)
        self._homePin.irq(handler=self._homeISR, trigger=machine.Pin.IRQ_RISING)

        self._position = 0.
        self._home = False
        
        self._disable()

    def _homeISR(self, pin):
        """
        """
        self._position = AUTO_HOMING_LONGITUDE
        self._home = True
    
    def _enable(self):
        """
        """
        self._enablePin.off()
    
    def _disable(self):
        """
        """
        self._enablePin.on()
    
    def _pulse(self):
        """
        """
        pulseStartTime = time.ticks_ms()
        self._stepPin.on()
        time.sleep_us(MOTOR_PULSE_LENGTH)
        self._stepPin.off()
        time.sleep_ms(MOTOR_PULSE_PERIOD-(time.ticks_ms()-pulseStartTime))
    
    def _step(self, steps=1):
        """

        'steps' sign is rotation direction.
        """
        if steps == 0:
            return
        
        elif steps / abs(steps) == +1:
            self._dirPin.value(MOTOR_CW_DIR)
            
        elif steps / abs(steps) == -1:
            self._dirPin.value(MOTOR_CCW_DIR)

        # Generate pulses
        for step in range(abs(steps)):
            self._pulse()
            
        # Update position
        self._position += steps / NB_STEPS_PER_DEGREE
        if self._position > 180:
            self._position -= 360
        elif self._position < -180:
            self._position += 360

    @property
    def position(self):
        return self._position
    
    def home(self):
        """
        """
        if HOMING == 'auto':
            print("LongitudeAxis axis homing... ", end='')

            self._home = False
            startTime = time.time()
            self._enable()
            try:            
                while not self._home:
                    self._step(+1)
                    
                    # Manage timeout
                    if time.time() - startTime >= TIMEOUT:
                        raise Exception("Timeout occured during longitude axis homing")

            finally:
#                 self._disable()
                pass
        
        elif HOMING == 'manual':
            print("Turn globe to align Greenwitch meridian...", end='')
            time.sleep(MANUAL_HOMING_DELAY)
            self._home = True
            self._position = 0
        
        else:
            raise ValueError("Unknown homing method")

        print(" done.")
        print("Now at longitude: {:8.4f}°".format(self._position))

    def move(self, target):
        """
        """
        if abs(round(target - self._position, 1)) <= MOTOR_ACCURACY:
            print("Current longitude close enough to target")
            return

        self._enable()
        try:
            if self._home:
                print("Initial move", end='')
                
                # Set rotation direction for shortest path
                if abs(target - self._position) <= 360 - abs(target) - abs(self._position):
                    steps = round((target - self._position) * NB_STEPS_PER_DEGREE)
                    
                else:
                    steps = round((360 - abs(target) - abs(self._position)) * NB_STEPS_PER_DEGREE)
                    if target >= 0:
                        steps *= -1 
                    
                print(f" ({steps} steps)...", end='')
                self._step(steps)
                print(" New longitude: {:8.4f}°".format(self._position))
                
                self._home = False

            else:
                print("Tracking", end='')

                # Move until target reached
                startTime = time.time()
                while abs(round(self._position - target, 1)) > MOTOR_ACCURACY:
                    self._step(+1)  # rotation dir is always CW
                    print(".", end='')
                
                    # Recalibrate when hitting home switch. TODO
                    if self._home:
                        self._home = False
                        
                    # Manage timeout
                    if time.time() - startTime >= TIMEOUT:
                        raise Exception("Timeout occured during longitude axis move")
                        
                print(" New longitude: {:8.4f}°".format(self._position))

        finally:
#             self._disable()
            pass


class LatitudeAxis:
    """
    """
    def __init__(self):
        """
        """
        self._servo = machine.PWM(machine.Pin(SERVO_PIN))
        self._servo.freq(SERVO_PWM_FREQ)
        
        self._position = None
        self.move(0)
        
    @property
    def position(self):
        return self._position
 
    def move(self, target):
        """
        """
        target = round(target, 1)
        if target == self._position:
            return
        
        pulse = target * (SERVO_PULSE_MIN - SERVO_PULSE_MAX) / (SERVO_LATITUDE_MIN - SERVO_LATITUDE_MAX) + (SERVO_PULSE_MIN + SERVO_PULSE_MAX) / 2
        self._servo.duty_ns(int(pulse*1000))
        
        self._position = target
    

class Pointer:
    """
    """
    def __init__(self):
        """
        """
        self._pin = machine.Pin(POINTER_PIN, machine.Pin.OUT)
        self._timer = machine.Timer(0)
        
        self.off()
        
    def _timerISR(self, t):
        """

        Invert pin state.
        """
        self._pin.value(not self._pin.value())
        
    def on(self):
        """
        """
        self._timer.deinit()
        self._pin.on()
        
    def off(self):
        """
        """
        self._timer.deinit()
        self._pin.off()
        
    def blink(self, freq=1):
        """
        """
        self._timer.init(freq=freq*2, callback=self._timerISR)
    

def main():
    """
    """
    longitudeAxis = LongitudeAxis()
    latitudeAxis = LatitudeAxis()
    pointer = Pointer()

    iss = ISS(URL)

    wifi = Wifi(SSID, PASSWD)
    
    # Init
    if HOMING == 'manual':
        latitudeAxis.move(GREENWICH_LATITUDE)
        pointer.blink(MANUAL_HOMING_BLINK_FREQ)

    longitudeAxis.home()
    pointer.off()
    time.sleep(1)
    
    while True:
        pointer.blink(1)
        wifi.connect()

        while True:
            try:
                pointer.off()
                date, longitude, latitude = iss.getPosition()
                latitudeAxis.move(latitude)
                longitudeAxis.move(longitude)
                time.sleep(1)  # wait for servo to reach its position
                pointer.on()
            
            except OSError as e:
                print(e)
                break  # force a new connection
            
            except Exception as e:
                print(e)
            
            time.sleep(REFRESH_DELAY)
    

if __name__ == "__main__":
    main()

Axes d'amélioration

  • ajouter des retours d'infos en faisant clignoter le pointer
  • réaliser un PCB pour tout intégrer (y compris le capteur de homing)
  • faire une seule pièce imprimée en 3D pour toute la structure
  • utiliser un moteur DC + codeur incrémental ?
  • externaliser l'axe yaw (possibilité d'utiliser un moteur plus gros), et utiliser un collecteur tournant pour alimenter le servo et le laser
  • utiliser un globe plus gros et plus joli, avec des LEDs internes
  • faire une interface web permettant d'ajouter des fonctionnalités (tracker d'autres objets en orbite, retrouver un pays, etc...)
  • mettre le pointeur à l'extérieur, et faire tourner/basculer le globe sur 2 axes. Cela permet d'utiliser des globes lumineux qui font apparaître différents type de carte (pays/reliefs) suivant l'éclairage (comme le superbe - mais cher - Colombus Satellite Duorama 34cm)

Liens