]> www.average.org Git - loctrkd.git/commitdiff
Implement sending commands from the web interface
authorEugene Crosser <crosser@average.org>
Sat, 26 Nov 2022 23:05:37 +0000 (00:05 +0100)
committerEugene Crosser <crosser@average.org>
Sun, 27 Nov 2022 20:14:01 +0000 (21:14 +0100)
loctrkd/beesure.py
loctrkd/common.py
loctrkd/evstore.py
loctrkd/protomodule.py
loctrkd/rectifier.py
loctrkd/storage.py
loctrkd/wsgateway.py
loctrkd/zmsg.py
loctrkd/zx303proto.py

index dcf16ba227fd7ca585baa9371595e19307917fe9..e782c736189ca5d74a3f0cc188c7973672660c13 100755 (executable)
@@ -43,6 +43,7 @@ __all__ = (
     "Respond",
 )
 
     "Respond",
 )
 
+MODNAME = __name__.split(".")[-1]
 PROTO_PREFIX = "BS:"
 
 ### Deframer ###
 PROTO_PREFIX = "BS:"
 
 ### Deframer ###
@@ -380,12 +381,12 @@ class _LOC_DATA(BeeSurePkt):
         self.latitude = p.lat * p.nors
         self.longitude = p.lon * p.eorw
 
         self.latitude = p.lat * p.nors
         self.longitude = p.lon * p.eorw
 
-    def rectified(self) -> Report:
+    def rectified(self) -> Tuple[str, Report]:
         # self.gps_valid is supposed to mean it, but it does not. Perfectly
         # good looking coordinates, with ten satellites, still get 'V'.
         # I suspect that in reality, 'A' means "hint data is absent".
         if self.gps_valid or self.num_of_sats > 3:
         # self.gps_valid is supposed to mean it, but it does not. Perfectly
         # good looking coordinates, with ten satellites, still get 'V'.
         # I suspect that in reality, 'A' means "hint data is absent".
         if self.gps_valid or self.num_of_sats > 3:
-            return CoordReport(
+            return MODNAME, CoordReport(
                 devtime=str(self.devtime),
                 battery_percentage=self.battery_percentage,
                 accuracy=self.positioning_accuracy,
                 devtime=str(self.devtime),
                 battery_percentage=self.battery_percentage,
                 accuracy=self.positioning_accuracy,
@@ -396,7 +397,7 @@ class _LOC_DATA(BeeSurePkt):
                 longitude=self.longitude,
             )
         else:
                 longitude=self.longitude,
             )
         else:
-            return HintReport(
+            return MODNAME, HintReport(
                 devtime=str(self.devtime),
                 battery_percentage=self.battery_percentage,
                 mcc=self.mcc,
                 devtime=str(self.devtime),
                 battery_percentage=self.battery_percentage,
                 mcc=self.mcc,
@@ -679,3 +680,13 @@ def exposed_protos() -> List[Tuple[str, bool]]:
         for cls in CLASSES.values()
         if hasattr(cls, "rectified")
     ]
         for cls in CLASSES.values()
         if hasattr(cls, "rectified")
     ]
+
+
+def make_response(cmd: str, imei: str, **kwargs: Any) -> Optional[BeeSurePkt]:
+    if cmd == "poweroff":
+        return POWEROFF.Out()
+    elif cmd == "refresh":
+        return MONITOR.Out()
+    elif cmd == "message":
+        return MESSAGE.Out(message=kwargs.get("txt", "Hello"))
+    return None
index 941a93e008dbf793cad604d0df1fd86a81809b63..162fe0db07abbbd5a14e0cfbc429d93d193eb9d5 100644 (file)
@@ -11,7 +11,7 @@ from sys import argv, stderr, stdout
 from typing import Any, cast, Dict, List, Optional, Tuple, Union
 from types import SimpleNamespace
 
 from typing import Any, cast, Dict, List, Optional, Tuple, Union
 from types import SimpleNamespace
 
-from .protomodule import ProtoModule
+from .protomodule import ProtoClass, ProtoModule
 
 CONF = "/etc/loctrkd.conf"
 pmods: List[ProtoModule] = []
 
 CONF = "/etc/loctrkd.conf"
 pmods: List[ProtoModule] = []
@@ -71,6 +71,22 @@ def pmod_for_proto(proto: str) -> Optional[ProtoModule]:
     return None
 
 
     return None
 
 
+def pmod_by_name(pmodname: str) -> Optional[ProtoModule]:
+    for pmod in pmods:
+        if pmod.__name__.split(".")[-1] == pmodname:
+            return pmod
+    return None
+
+
+def make_response(
+    pmodname: str, cmd: str, imei: str, **kwargs: Any
+) -> Optional[ProtoClass.Out]:
+    pmod = pmod_by_name(pmodname)
+    if pmod is None:
+        return None
+    return pmod.make_response(cmd, imei, **kwargs)
+
+
 def parse_message(proto: str, packet: bytes, is_incoming: bool = True) -> Any:
     pmod = pmod_for_proto(proto)
     return pmod.parse_message(packet, is_incoming) if pmod else None
 def parse_message(proto: str, packet: bytes, is_incoming: bool = True) -> Any:
     pmod = pmod_for_proto(proto)
     return pmod.parse_message(packet, is_incoming) if pmod else None
index 85e8c9d25767de36fbf4e30444c9caa6f7bd6d5c..6d6edd018fc7d46747e9de26003870d222af0de6 100644 (file)
@@ -3,7 +3,7 @@
 from datetime import datetime
 from json import dumps, loads
 from sqlite3 import connect, OperationalError, Row
 from datetime import datetime
 from json import dumps, loads
 from sqlite3 import connect, OperationalError, Row
-from typing import Any, Dict, List, Tuple
+from typing import Any, Dict, List, Optional, Tuple
 
 __all__ = "fetch", "initdb", "stow", "stowloc"
 
 
 __all__ = "fetch", "initdb", "stow", "stowloc"
 
@@ -25,6 +25,10 @@ SCHEMA = (
     latitude real,
     longitude real,
     remainder text
     latitude real,
     longitude real,
     remainder text
+)""",
+    """create table if not exists pmodmap (
+    imei text not null unique,
+    pmod text not null
 )""",
 )
 
 )""",
 )
 
@@ -86,6 +90,17 @@ def stowloc(**kwargs: Dict[str, Any]) -> None:
     DB.commit()
 
 
     DB.commit()
 
 
+def stowpmod(imei: str, pmod: str) -> None:
+    assert DB is not None
+    DB.execute(
+        """insert or replace into pmodmap
+                (imei, pmod) values (:imei, :pmod)
+        """,
+        {"imei": imei, "pmod": pmod},
+    )
+    DB.commit()
+
+
 def fetch(imei: str, backlog: int) -> List[Dict[str, Any]]:
     assert DB is not None
     cur = DB.cursor()
 def fetch(imei: str, backlog: int) -> List[Dict[str, Any]]:
     assert DB is not None
     cur = DB.cursor()
@@ -103,3 +118,15 @@ def fetch(imei: str, backlog: int) -> List[Dict[str, Any]]:
         result.append(dic)
     cur.close()
     return list(reversed(result))
         result.append(dic)
     cur.close()
     return list(reversed(result))
+
+
+def fetchpmod(imei: str) -> Optional[Any]:
+    assert DB is not None
+    ret = None
+    cur = DB.cursor()
+    cur.execute("select pmod from pmodmap where imei = ?", (imei,))
+    result = cur.fetchone()
+    if result:
+        ret = result[0]
+    cur.close()
+    return ret
index 1d61759c19239b9e774e2bdc811eb6d53dd4fc19..a1698f2fde7ae52389a62b16dccf265122bacc93 100644 (file)
@@ -122,6 +122,8 @@ class ProtoClass(Protocol, metaclass=_MetaProto):
 
 
 class ProtoModule:
 
 
 class ProtoModule:
+    __name__: str
+
     class Stream:
         def recv(self, segment: bytes) -> List[Union[bytes, str]]:
             ...
     class Stream:
         def recv(self, segment: bytes) -> List[Union[bytes, str]]:
             ...
@@ -171,3 +173,9 @@ class ProtoModule:
     @staticmethod
     def class_by_prefix(prefix: str) -> Union[Type[ProtoClass], List[str]]:
         ...
     @staticmethod
     def class_by_prefix(prefix: str) -> Union[Type[ProtoClass], List[str]]:
         ...
+
+    @staticmethod
+    def make_response(
+        cmd: str, imei: str, **kwargs: Any
+    ) -> Optional[ProtoClass.Out]:
+        ...
index 585eb793d223ad37eab4fc7a2a04864f3d673c3e..5926377ee76ad4dc5665b72dec551a5e279bc661 100644 (file)
@@ -68,10 +68,12 @@ def runserver(conf: ConfigParser) -> None:
                 datetime.fromtimestamp(zmsg.when).astimezone(tz=timezone.utc),
                 msg,
             )
                 datetime.fromtimestamp(zmsg.when).astimezone(tz=timezone.utc),
                 msg,
             )
-            rect: Report = msg.rectified()
+            pmod, rect = msg.rectified()
             log.debug("rectified: %s", rect)
             if isinstance(rect, (CoordReport, StatusReport)):
             log.debug("rectified: %s", rect)
             if isinstance(rect, (CoordReport, StatusReport)):
-                zpub.send(Rept(imei=zmsg.imei, payload=rect.json).packed)
+                zpub.send(
+                    Rept(imei=zmsg.imei, pmod=pmod, payload=rect.json).packed
+                )
             elif isinstance(rect, HintReport):
                 try:
                     lat, lon, acc = qry.lookup(
             elif isinstance(rect, HintReport):
                 try:
                     lat, lon, acc = qry.lookup(
index 5ea3b2c823c15618ed2845e6633b384fd2f93289..e1be227d8ef8b11d1b0b05586f4cba2dab19f2a1 100644 (file)
@@ -7,7 +7,7 @@ from logging import getLogger
 import zmq
 
 from . import common
 import zmq
 
 from . import common
-from .evstore import initdb, stow, stowloc
+from .evstore import initdb, stow, stowloc, stowpmod
 from .zmsg import Bcast, Rept
 
 log = getLogger("loctrkd/storage")
 from .zmsg import Bcast, Rept
 
 log = getLogger("loctrkd/storage")
@@ -64,6 +64,8 @@ def runserver(conf: ConfigParser) -> None:
                             rept = Rept(zrep.recv(zmq.NOBLOCK))
                         except zmq.Again:
                             break
                             rept = Rept(zrep.recv(zmq.NOBLOCK))
                         except zmq.Again:
                             break
+                        if rept.imei is not None and rept.pmod is not None:
+                            stowpmod(rept.imei, rept.pmod)
                         data = loads(rept.payload)
                         log.debug("R IMEI %s %s", rept.imei, data)
                         if data.pop("type") == "location":
                         data = loads(rept.payload)
                         log.debug("R IMEI %s %s", rept.imei, data)
                         if data.pop("type") == "location":
index e94845eec4215e04797bde4e0dce43b05e8780ec..c5dcf5a785a03d13e8748bbfb441afec5b21492d 100644 (file)
@@ -22,9 +22,9 @@ from wsproto.utilities import RemoteProtocolError
 import zmq
 
 from . import common
 import zmq
 
 from . import common
-from .evstore import initdb, fetch
+from .evstore import initdb, fetch, fetchpmod
 from .protomodule import ProtoModule
 from .protomodule import ProtoModule
-from .zmsg import Rept, rtopic
+from .zmsg import Rept, Resp, rtopic
 
 log = getLogger("loctrkd/wsgateway")
 
 
 log = getLogger("loctrkd/wsgateway")
 
@@ -101,6 +101,9 @@ class Client:
         self.ready = False
         self.imeis: Set[str] = set()
 
         self.ready = False
         self.imeis: Set[str] = set()
 
+    def __str__(self) -> str:
+        return f"{self.__class__.__name__}(fd={self.sock.fileno()}, addr={self.addr})"
+
     def close(self) -> None:
         log.debug("Closing fd %d", self.sock.fileno())
         self.sock.close()
     def close(self) -> None:
         log.debug("Closing fd %d", self.sock.fileno())
         self.sock.close()
@@ -247,6 +250,42 @@ class Clients:
         return result
 
 
         return result
 
 
+def sendcmd(zpush: Any, wsmsg: Dict[str, Any]) -> Dict[str, Any]:
+    imei = wsmsg.pop("imei", None)
+    cmd = wsmsg.pop("type", None)
+    if imei is None or cmd is None:
+        log.info("Unhandled message %s %s %s", cmd, imei, wsmsg)
+        return {
+            "type": "cmdresult",
+            "imei": imei,
+            "result": "Did not get imei or cmd",
+        }
+    pmod = fetchpmod(imei)
+    if pmod is None:
+        log.info("Uknown type of recipient for %s %s %s", cmd, imei, wsmsg)
+        return {
+            "type": "cmdresult",
+            "imei": imei,
+            "result": "Type of the terminal is unknown",
+        }
+    tmsg = common.make_response(pmod, cmd, imei, **wsmsg)
+    if tmsg is None:
+        log.info("Could not make packet for %s %s %s", cmd, imei, wsmsg)
+        return {
+            "type": "cmdresult",
+            "imei": imei,
+            "result": f"{cmd} unimplemented for terminal protocol {pmod}",
+        }
+    resp = Resp(imei=imei, when=time(), packet=tmsg.packed)
+    log.debug("Response: %s", resp)
+    zpush.send(resp.packed)
+    return {
+        "type": "cmdresult",
+        "imei": imei,
+        "result": f"{cmd} sent to {imei}",
+    }
+
+
 def runserver(conf: ConfigParser) -> None:
     global htmlfile
     initdb(conf.get("storage", "dbfn"))
 def runserver(conf: ConfigParser) -> None:
     global htmlfile
     initdb(conf.get("storage", "dbfn"))
@@ -255,6 +294,8 @@ def runserver(conf: ConfigParser) -> None:
     zctx = zmq.Context()  # type: ignore
     zsub = zctx.socket(zmq.SUB)  # type: ignore
     zsub.connect(conf.get("rectifier", "publishurl"))
     zctx = zmq.Context()  # type: ignore
     zsub = zctx.socket(zmq.SUB)  # type: ignore
     zsub.connect(conf.get("rectifier", "publishurl"))
+    zpush = zctx.socket(zmq.PUSH)  # type: ignore
+    zpush.connect(conf.get("collector", "listenurl"))
     tcpl = socket(AF_INET6, SOCK_STREAM)
     tcpl.setblocking(False)
     tcpl.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
     tcpl = socket(AF_INET6, SOCK_STREAM)
     tcpl.setblocking(False)
     tcpl.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
@@ -315,6 +356,8 @@ def runserver(conf: ConfigParser) -> None:
                                             for msg in backlog(imei, numback)
                                         ]
                                     )
                                             for msg in backlog(imei, numback)
                                         ]
                                     )
+                            else:
+                                tosend.append((clnt, sendcmd(zpush, wsmsg)))
                         towrite.add(sk)
                 elif fl & zmq.POLLOUT:
                     log.debug("Write now open for fd %d", sk)
                         towrite.add(sk)
                 elif fl & zmq.POLLOUT:
                     log.debug("Write now open for fd %d", sk)
index da0dc77e39da6f38d7eaefe5adbdfe897f928b28..6539d5f18f67b8e3e88faa760ccbadb104af2861 100644 (file)
@@ -173,18 +173,21 @@ class Resp(_Zmsg):
 
 
 class Rept(_Zmsg):
 
 
 class Rept(_Zmsg):
-    """Broadcast Zzmq message with "rectified" proto-agnostic json data"""
+    """Broadcast zmq message with "rectified" proto-agnostic json data"""
 
 
-    KWARGS = (("imei", None), ("payload", ""))
+    KWARGS = (("imei", None), ("payload", ""), ("pmod", None))
 
     @property
     def packed(self) -> bytes:
         return (
             pack(
 
     @property
     def packed(self) -> bytes:
         return (
             pack(
-                "16s",
-                "0000000000000000"
+                "16s16s",
+                b"0000000000000000"
                 if self.imei is None
                 else self.imei.encode(),
                 if self.imei is None
                 else self.imei.encode(),
+                b"                "
+                if self.pmod is None
+                else self.pmod.encode(),
             )
             + self.payload.encode()
         )
             )
             + self.payload.encode()
         )
@@ -194,4 +197,8 @@ class Rept(_Zmsg):
         self.imei = (
             None if imei == b"0000000000000000" else imei.decode().strip("\0")
         )
         self.imei = (
             None if imei == b"0000000000000000" else imei.decode().strip("\0")
         )
-        self.payload = buffer[16:].decode()
+        pmod = buffer[16:32]
+        self.pmod = (
+            None if pmod == b"                " else pmod.decode().strip("\0")
+        )
+        self.payload = buffer[32:].decode()
index aff3405691baa596ca36ca90e00f7067880cef3f..a7329c316326187c950580ec11a65843981ace2d 100755 (executable)
@@ -48,6 +48,7 @@ __all__ = (
     "Respond",
 )
 
     "Respond",
 )
 
+MODNAME = __name__.split(".")[-1]
 PROTO_PREFIX: str = "ZX:"
 
 ### Deframer ###
 PROTO_PREFIX: str = "ZX:"
 
 ### Deframer ###
@@ -369,8 +370,8 @@ class _GPS_POSITIONING(GPS303Pkt):
         ttup = (tup[0] % 100,) + tup[1:6]
         return pack("BBBBBB", *ttup)
 
         ttup = (tup[0] % 100,) + tup[1:6]
         return pack("BBBBBB", *ttup)
 
-    def rectified(self) -> CoordReport:  # JSON-able dict
-        return CoordReport(
+    def rectified(self) -> Tuple[str, CoordReport]:  # JSON-able dict
+        return MODNAME, CoordReport(
             devtime=str(self.devtime),
             battery_percentage=None,
             accuracy=None,
             devtime=str(self.devtime),
             battery_percentage=None,
             accuracy=None,
@@ -419,8 +420,8 @@ class STATUS(GPS303Pkt):
     def out_encode(self) -> bytes:  # Set interval in minutes
         return pack("B", self.upload_interval)
 
     def out_encode(self) -> bytes:  # Set interval in minutes
         return pack("B", self.upload_interval)
 
-    def rectified(self) -> StatusReport:
-        return StatusReport(battery_percentage=self.batt)
+    def rectified(self) -> Tuple[str, StatusReport]:
+        return MODNAME, StatusReport(battery_percentage=self.batt)
 
 
 class HIBERNATION(GPS303Pkt):  # Server can send to send devicee to sleep
 
 
 class HIBERNATION(GPS303Pkt):  # Server can send to send devicee to sleep
@@ -500,8 +501,8 @@ class _WIFI_POSITIONING(GPS303Pkt):
             ]
         )
 
             ]
         )
 
-    def rectified(self) -> HintReport:
-        return HintReport(
+    def rectified(self) -> Tuple[str, HintReport]:
+        return MODNAME, HintReport(
             devtime=str(self.devtime),
             battery_percentage=None,
             mcc=self.mcc,
             devtime=str(self.devtime),
             battery_percentage=None,
             mcc=self.mcc,
@@ -895,3 +896,9 @@ def exposed_protos() -> List[Tuple[str, bool]]:
         for cls in CLASSES.values()
         if hasattr(cls, "rectified")
     ]
         for cls in CLASSES.values()
         if hasattr(cls, "rectified")
     ]
+
+
+def make_response(cmd: str, imei: str, **kwargs: Any) -> Optional[GPS303Pkt]:
+    if cmd == "poweroff":
+        return HIBERNATION.Out()
+    return None