diff --git a/config/instruction_images/st-ten-1/000825276.svg b/config/instruction_images/st-ten-1/000825276.svg
new file mode 100644
index 0000000..03dcb32
--- /dev/null
+++ b/config/instruction_images/st-ten-1/000825276.svg
@@ -0,0 +1,70 @@
+
+
+
+
diff --git a/config/instruction_images/st-ten-1/DEFAULT.svg b/config/instruction_images/st-ten-1/DEFAULT.svg
new file mode 100644
index 0000000..facc0bb
--- /dev/null
+++ b/config/instruction_images/st-ten-1/DEFAULT.svg
@@ -0,0 +1,49 @@
+
+
+
+
diff --git a/config/instruction_images/st-ten-1/img/arw-yel-down.png b/config/instruction_images/st-ten-1/img/arw-yel-down.png
new file mode 100644
index 0000000..bb3c668
Binary files /dev/null and b/config/instruction_images/st-ten-1/img/arw-yel-down.png differ
diff --git a/config/instruction_images/st-ten-1/img/ok.png b/config/instruction_images/st-ten-1/img/ok.png
new file mode 100644
index 0000000..d576b7d
Binary files /dev/null and b/config/instruction_images/st-ten-1/img/ok.png differ
diff --git a/config/instruction_images/st-ten-1/img/tape_black.png b/config/instruction_images/st-ten-1/img/tape_black.png
new file mode 100644
index 0000000..0d51c4b
Binary files /dev/null and b/config/instruction_images/st-ten-1/img/tape_black.png differ
diff --git a/config/instruction_images/st-ten-1/img/tape_white.png b/config/instruction_images/st-ten-1/img/tape_white.png
new file mode 100644
index 0000000..d4b64f0
Binary files /dev/null and b/config/instruction_images/st-ten-1/img/tape_white.png differ
diff --git a/config/machine_settings/st-ten-1.ini b/config/machine_settings/st-ten-1.ini
index c669810..830e439 100644
--- a/config/machine_settings/st-ten-1.ini
+++ b/config/machine_settings/st-ten-1.ini
@@ -14,10 +14,11 @@ remote_api: absent
tecna_t3: present
vision_saver: absent
vision: absent
-screwdriver: absent
-#fixture_id: present
+screwdriver: present
+fixture_id: present
digital_io: present
external_flush_blow: absent
+barcode_recipe_selection: present
# VERO PROJECT LOCAL SERVER
[archive_synchronizer_extra]
@@ -104,9 +105,9 @@ settling_pressure_min_percent: 5
settling_pressure_max_percent: 5
test_pressure: 7000
test_time: 10
-test_pressure_qpos: 10 #Q+ Upper test leak limit
-test_pressure_qneg: 30 #Q- Lower test leak limit
-test_pressure_tt_qpos: 1 # Q+ Upper test leak limit (tube-tube)
+test_pressure_qpos: 5 #Q+ Upper test leak limit
+test_pressure_qneg: 17 #Q- Lower test leak limit
+test_pressure_tt_qpos: 5 # Q+ Upper test leak limit (tube-tube)
test_pressure_tt_qneg: 5 # Q- Lower test leak limit (tube-tube)
flush_time: 1
flush_pressure: 100
diff --git a/src/components/archive_synchronizer.py b/src/components/archive_synchronizer.py
index 19eeeb5..2ee36a0 100644
--- a/src/components/archive_synchronizer.py
+++ b/src/components/archive_synchronizer.py
@@ -19,6 +19,8 @@ from PyQt5.QtCore import QThread
from requests.adapters import HTTPAdapter, Retry
from urllib3.exceptions import InsecureRequestWarning
+from lib.helpers.recipe_manager import import_recipes, backup_current_recipes
+
from .component import Component
from ui.helpers import get_main_window
# Suppress insecure request warning
@@ -135,6 +137,9 @@ class ArchiveSynchronizer(Component):
return True
def parse_response_and_execute(self, response):
+ """
+ Parse the response and execute actions based on the `ACTIONS_TO_DO` received.
+ """
try:
data = response.json()
if not isinstance(data, dict):
@@ -147,11 +152,53 @@ class ArchiveSynchronizer(Component):
actions = [actions]
for action in actions:
- remote_path = action.get("remote_path")
- local_path = action.get("local_path")
- self.log.info(f"Executing remote fetch with remote_path: {remote_path} and local_path: {local_path}")
- result = self.remote_fetch(remote_path=remote_path, local_path=local_path)
- self.log.info(f"Remote fetch result: {result}")
+ action_type = action.get("action") # Determine which type of action to perform
+
+ if action_type == "import": # Handle import action
+ remote_path = action.get("remote_path")
+ if not remote_path:
+ self.log.warning("Import action received without a remote_path.")
+ continue
+
+ # Use remote_fetch to download the recipe file from the server
+ fetch_result = self.remote_fetch(remote_path=remote_path, local_path="tmp")
+ if 'downloaded_file' in fetch_result:
+ downloaded_file = fetch_result['downloaded_file']
+
+ self.log.info(f"Recipe file downloaded successfully to {downloaded_file}.")
+
+ # Perform the import action
+ try:
+ # Backup current recipes before importing
+ backup_path = backup_current_recipes(
+ config=self.config, # Backup configuration object
+ logger=self.log # Logger for backup messages
+ )
+ self.log.info(f"Backup created successfully at {backup_path}.")
+
+ # Proceed with importing recipes
+ import_recipes(
+ config=self.config,
+ csv_path=downloaded_file, # Use the downloaded file path
+ logger=self.log
+ )
+ self.log.info(f"Imported recipes successfully from {downloaded_file}.")
+ except Exception as e:
+ self.log.error(f"Failed to import recipes: {str(e)}")
+ continue
+ else:
+ self.log.warning(f"Failed to fetch the recipe file: {fetch_result.get('error')}.")
+
+ elif action_type == "download": # Handle fetch action
+ remote_path = action.get("remote_path")
+ local_path = action.get("local_path", "tmp") # Use "tmp" as a fallback local path
+ self.log.info(
+ f"Executing remote fetch with remote_path: {remote_path} and local_path: {local_path}")
+ result = self.remote_fetch(remote_path=remote_path, local_path=local_path)
+ self.log.info(f"Remote fetch result: {result}")
+
+ else:
+ self.log.warning(f"Unhandled action type: {action_type}")
except json.JSONDecodeError:
self.log.error("Failed to decode JSON response")
@@ -283,8 +330,10 @@ class ArchiveSynchronizer(Component):
self.log_to_db(log_time, log_info_type, log_info)
return {"error": "Unexpected HTTP response status", "last_update_info": last_update_info}
+ # Make sure the local path exists
os.makedirs(local_path, exist_ok=True)
+ # Construct the correct file path
local_file_path = os.path.join(local_path, os.path.basename(remote_path))
with open(local_file_path, "wb") as f:
f.write(response.content)
@@ -292,6 +341,7 @@ class ArchiveSynchronizer(Component):
log_info += f" - File downloaded successfully: {local_file_path}"
self.log.info(log_info)
self.log_to_db(log_time, log_info_type, log_info)
+
return {"downloaded_file": local_file_path, "last_update_info": last_update_info}
except requests.ConnectionError as e:
diff --git a/src/components/rfid_pn532.py b/src/components/rfid_pn532.py
index a066754..241d79e 100644
--- a/src/components/rfid_pn532.py
+++ b/src/components/rfid_pn532.py
@@ -5,8 +5,8 @@ import platform
from PyQt5.QtCore import QMutex, Qt, QTimer, pyqtSlot, pyqtSignal
from .component import Component
import ndef
-import nfc
-from nfc.clf import RemoteTarget
+import src.lib.nfc
+from src.lib.nfc.clf import RemoteTarget
class RFID_PN532(Component):
@@ -26,7 +26,7 @@ class RFID_PN532(Component):
self._period = 1
def open_device(self):
- self.clf = nfc.ContactlessFrontend()
+ self.clf = src.lib.nfc.ContactlessFrontend()
for dev in self.dev_list:
self.connected = self.clf.open(dev)
if self.connected:
@@ -52,7 +52,7 @@ class RFID_PN532(Component):
else:
target = self.clf.sense(RemoteTarget('106A'), RemoteTarget('106B'), RemoteTarget('212F'))
if target is not None:
- tag = nfc.tag.activate(self.clf, target)
+ tag = src.lib.nfc.tag.activate(self.clf, target)
if tag is not None:
self.log.debug("tag present")
if tag.ndef is not None:
diff --git a/src/lib/__init__.py b/src/lib/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/lib/__init__.py
@@ -0,0 +1 @@
+
diff --git a/src/lib/helpers/recipe_manager.py b/src/lib/helpers/recipe_manager.py
new file mode 100644
index 0000000..7b6aa79
--- /dev/null
+++ b/src/lib/helpers/recipe_manager.py
@@ -0,0 +1,405 @@
+import os
+import csv
+import locale
+from datetime import datetime
+import shutil
+from PyQt5.QtWidgets import QFileDialog
+from lib.db import Recipes, db # Assuming these are part of your project structure
+
+
+def read_steps(row, config, defaults=None, unsupported_steps=None):
+ if defaults is None:
+ defaults = config.get("recipes_defaults", lambda k: None)
+
+ # Configurable fields from the config object
+ barcode_serial_field = config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip()
+ warning_image_field = config.get("recipe", {}).get("warning_image_field", "warning_img").strip()
+ print_template_field = config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip()
+ decsep = locale.localeconv()["decimal_point"]
+
+ # Extract and clean "r nominale" value
+ rcsv = (
+ row.get("r nominale", defaults["r nominale"])
+ .replace(" ", "").replace(",", decsep).replace("Ω", "").replace("?", "")
+ )
+ if rcsv == "":
+ rcsv = "999" # Default fallback for "r nominale" if empty
+
+ # Helper functions
+ def get_default_value(field, key):
+ value = field.get(key, defaults[key])
+ return value if value != "" else defaults[key]
+
+ def safe_parse(value):
+ try:
+ return int(float(value))
+ except ValueError:
+ return 0 # Default to 0 if parsing fails
+
+ # Define the steps dictionary
+ steps = {
+ "count": {
+ "amount": row.get("dimensione_lotto", defaults["dimensione_lotto"]),
+ "warning_img": row.get(warning_image_field, defaults["warning_img"]),
+ "require_discard_piece": row.get("richiedi_inserimento_scarto", defaults["richiedi_inserimento_scarto"]),
+ },
+ "connector": {
+ "connector": row.get("connettore", defaults["connettore"]),
+ },
+ "barcodes": {
+ "serial": row.get(barcode_serial_field, defaults["codice_a_barre"]),
+ "n_pieces": row.get("n_componenti", defaults["n_componenti"]),
+ "barcode_input_2": row.get("barcode_input_2", "-"),
+ "barcode_input_3": row.get("barcode_input_3", "-"),
+ "barcode_input_4": row.get("barcode_input_4", "-"),
+ "barcode_input_5": row.get("barcode_input_5", "-"),
+ },
+ "resistance": {
+ "scale": locale.atof(row.get("scala_resistenza", defaults["scala_resistenza"])),
+ "expected": locale.atof(rcsv),
+ "tolerance_pos": locale.atof(get_default_value(row, "tolleranza_resistenza_pos")),
+ "tolerance_neg": locale.atof(get_default_value(row, "tolleranza_resistenza_neg")),
+ },
+ "screws": {
+ "quantity": row.get("viti", defaults["viti"]),
+ },
+ "instruction": {}, # Empty placeholder for future extensions
+ "leak_1": {
+ "pre_filling_time": safe_parse(row.get("tempo_pre_riempimento", defaults["tempo_pre_riempimento"])),
+ "pre_filling_pressure": safe_parse(
+ row.get("pressione_pre_riempimento", defaults["pressione_pre_riempimento"])),
+ "filling_time": safe_parse(row.get("tempo_riempimento", defaults["tempo_riempimento"])),
+ "settling_time": safe_parse(get_default_value(row, "tempo_assestamento")),
+ "settling_pressure_min_percent": safe_parse(
+ row.get("percentuale_minima_pressione_assestamento",
+ defaults["percentuale_minima_pressione_assestamento"])
+ ),
+ "settling_pressure_max_percent": safe_parse(
+ row.get("percentuale_massima_pressione_assestamento",
+ defaults["percentuale_massima_pressione_assestamento"])
+ ),
+ "test_time": safe_parse(row.get("tempo_di_test", defaults["tempo_di_test"])),
+ "test_pressure_qneg": safe_parse(
+ row.get("pressione_di_test_delta_minimo", defaults["pressione_di_test_delta_minimo"])),
+ "test_pressure": safe_parse(row.get("pressione_di_test", defaults["pressione_di_test"])),
+ "test_pressure_qpos": safe_parse(
+ row.get("pressione_di_test_delta_massimo", defaults["pressione_di_test_delta_massimo"])),
+ "flush_time": safe_parse(row.get("tempo_svuotamento", defaults["tempo_svuotamento"])),
+ "flush_pressure": safe_parse(row.get("pressione_svuotamento", defaults["pressione_svuotamento"])),
+ "chan_sel": safe_parse(row.get("canale_di_prova", defaults["canale_di_prova"])),
+ "ext_flush_time": safe_parse(row.get("tempo_svuotamento_esterno", defaults["tempo_svuotamento_esterno"])),
+ "ext_blow_time": safe_parse(row.get("tempo_soffiaggio_esterno", defaults["tempo_soffiaggio_esterno"])),
+ },
+ "leak_2": {
+ "pre_filling_time": safe_parse(row.get("tempo_pre_riempimento_2", defaults["tempo_pre_riempimento_2"])),
+ "pre_filling_pressure": safe_parse(
+ row.get("pressione_pre_riempimento_2", defaults["pressione_pre_riempimento_2"])),
+ "filling_time": safe_parse(row.get("tempo_riempimento_2", defaults["tempo_riempimento_2"])),
+ "settling_time": safe_parse(row.get("tempo_assestamento_2", defaults["tempo_assestamento_2"])),
+ "settling_pressure_min_percent": safe_parse(
+ row.get("percentuale_minima_pressione_assestamento_2",
+ defaults["percentuale_minima_pressione_assestamento_2"])
+ ),
+ "settling_pressure_max_percent": safe_parse(
+ row.get("percentuale_massima_pressione_assestamento_2",
+ defaults["percentuale_massima_pressione_assestamento_2"])
+ ),
+ "test_time": safe_parse(row.get("tempo_di_test_2", defaults["tempo_di_test_2"])),
+ "test_pressure_qneg": safe_parse(
+ row.get("pressione_di_test_delta_minimo_2", defaults["pressione_di_test_delta_minimo_2"])),
+ "test_pressure": safe_parse(row.get("pressione_di_test_2", defaults["pressione_di_test_2"])),
+ "test_pressure_qpos": safe_parse(
+ row.get("pressione_di_test_delta_massimo_2", defaults["pressione_di_test_delta_massimo_2"])),
+ "flush_time": safe_parse(row.get("tempo_svuotamento_2", defaults["tempo_svuotamento_2"])),
+ "flush_pressure": safe_parse(row.get("pressione_svuotamento_2", defaults["pressione_svuotamento_2"])),
+ "chan_sel": safe_parse(row.get("canale_di_prova_2", defaults["canale_di_prova_2"])),
+ "ext_flush_time": safe_parse(row.get("tempo_svuotamento_esterno_2", defaults["tempo_svuotamento_esterno"])),
+ "ext_blow_time": safe_parse(row.get("tempo_soffiaggio_esterno_2", defaults["tempo_soffiaggio_esterno"])),
+ },
+ "vision": {
+ "recipe": row.get("ricetta_visione", defaults["ricetta_visione"]),
+ },
+ "print": {
+ "template": row.get(print_template_field, defaults["modello_etichetta"]),
+ "labeltxt_1": row.get("testo_etich_1", ""),
+ "labeltxt_2": row.get("testo_etich_2", ""),
+ "labeltxt_3": row.get("testo_etich_3", ""),
+ "labeltxt_4": row.get("testo_etich_4", ""),
+ "labeltxt_5": row.get("barcode_input_finelinea", ""),
+ "extra_label": row.get("etichette_supplementari", ""),
+ },
+ }
+
+ # Remove unsupported steps if specified
+ if unsupported_steps:
+ for step in unsupported_steps:
+ steps.pop(step, None)
+
+ return steps
+
+
+
+def import_recipes(config, csv_path=None, defaults=None, unsupported_steps=None, logger=None):
+ """
+ Import recipes from CSV and update or create new ones in the database.
+
+ :param config: Configuration object with recipe settings.
+ :param csv_path: Path to the CSV file (optional). If None, a file dialog will open.
+ :param defaults: Default values to use for missing fields in the CSV.
+ :param unsupported_steps: A list of unsupported step names to exclude.
+ :param logger: Logger object for logging messages (optional).
+ """
+ if defaults is None:
+ defaults = config.get("recipes_defaults", lambda k: None)
+
+ # Open file dialog if csv_path is not provided
+ if csv_path is None:
+ options = QFileDialog.Options()
+ options |= QFileDialog.DontUseNativeDialog
+ csv_path, _ = QFileDialog.getOpenFileName(
+ None,
+ "Import Recipes",
+ "recipes.csv",
+ "CSV files (*.csv);;All Files (*)",
+ options=options,
+ )
+ csv_path = str(csv_path)
+ if not len(csv_path):
+ return
+
+ if logger:
+ logger.info(f"Importing recipes from: {csv_path}.")
+
+ # Get field mappings from the config
+ recipe_name_field = config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip()
+ part_number_field = config.get("recipe", {}).get("part_number_field", "part_number").strip()
+ description_field = config.get("recipe", {}).get("description_field", "descrizione").strip()
+ barcode_enable_field = config.get(
+ "recipe", {}
+ ).get("barcode_enable_field", "verifica_codice_a_barre_abilitata").strip()
+
+ with open(csv_path, "r", encoding="utf-8-sig") as file:
+ reader = csv.DictReader(file)
+ count = 0
+
+ for ucrow in reader:
+ # Normalize row keys to lowercase for consistency
+ row = dict((k.lower(), v) for k, v in ucrow.items())
+ recipe_name = row.get(recipe_name_field, defaults["codice_ricetta"])
+ steps_specs = read_steps(row, config, defaults=defaults, unsupported_steps=unsupported_steps)
+
+ # Create or update recipe in the database
+ try:
+ # Try to fetch existing recipe
+ recipe = Recipes.get_by_id(recipe_name)
+ recipe_is_new = False
+ except Recipes.DoesNotExist:
+ # Create a new recipe if it doesn't exist
+ recipe = Recipes(name=recipe_name, part_number="TEMPORARY")
+ recipe_is_new = True
+
+ # Update recipe attributes
+ recipe.client = row.get("cliente", defaults["cliente"])
+ recipe.part_number = row.get(part_number_field, defaults["part_number"])
+ recipe.description = row.get(description_field, defaults["descrizione"])
+
+ # Recipe specifications
+ steps = {}
+ for step_name, step_spec in steps_specs.items():
+ if unsupported_steps is None or step_name not in unsupported_steps:
+ steps[step_name] = step_spec
+
+ recipe.spec = {
+ "count": len(
+ row.get("dimensione_lotto_abilitata", defaults["dimensione_lotto_abilitata"])) and "count" not in (
+ unsupported_steps or []),
+ "connector": len(row.get("verifica_connettore_abilitata",
+ defaults["verifica_connettore_abilitata"])) and "connector" not in (
+ unsupported_steps or []),
+ "barcodes": len(row.get(barcode_enable_field,
+ defaults["verifica_codice_a_barre_abilitata"])) and "barcodes" not in (
+ unsupported_steps or []),
+ "resistance": len(row.get("verifica_resistenza_connettore_abilitata", defaults[
+ "verifica_resistenza_connettore_abilitata"])) and "resistance" not in (unsupported_steps or []),
+ "screws": len(row.get("avvitatura_abilitata", defaults["avvitatura_abilitata"])) and "screws" not in (
+ unsupported_steps or []),
+ "instruction": len(
+ row.get("istruzione_abilitata", defaults["istruzione_abilitata"])) and "instruction" not in (
+ unsupported_steps or []),
+ "instruction_extra": len(row.get("istruzione_abilitata_extra", defaults[
+ "istruzione_abilitata_extra"])) and "instruction_extra" not in (unsupported_steps or []),
+ "leak_1": len(
+ row.get("prova_tenuta_abilitata", defaults["prova_tenuta_abilitata"])) and "leak_1" not in (
+ unsupported_steps or []),
+ "leak_2": len(
+ row.get("prova_tenuta_abilitata_2", defaults["prova_tenuta_abilitata_2"])) and "leak_2" not in (
+ unsupported_steps or []),
+ "vision": len(
+ row.get("test_visione_abilitato", defaults["test_visione_abilitato"])) and "vision" not in (
+ unsupported_steps or []),
+ "print": len(
+ row.get("stampa_etichetta_abilitata", defaults["stampa_etichetta_abilitata"])) and "print" not in (
+ unsupported_steps or []),
+ "steps": steps_specs,
+ }
+
+ if recipe_is_new:
+ recipe.save(force_insert=True) # Insert new recipe
+ else:
+ recipe.save() # Update existing recipe
+
+ count += 1 # Increment imported recipe count
+
+ db.commit() # Commit all changes to the database
+
+ if logger:
+ logger.info(f"Imported {count} recipes.")
+
+
+
+def export_recipes(config, csv_path=None, logger=None):
+ if csv_path is None:
+ options = QFileDialog.Options()
+ options |= QFileDialog.DontUseNativeDialog
+ csv_path, _ = QFileDialog.getSaveFileName(
+ None,
+ "Export Recipes",
+ "recipes.csv",
+ "CSV files (*.csv);;All Files (*)",
+ options=options,
+ )
+ csv_path = str(csv_path)
+ if not len(csv_path):
+ return
+
+ if not csv_path.lower().endswith(".csv"):
+ csv_path += ".csv"
+ os.makedirs(os.path.dirname(csv_path), exist_ok=True)
+
+ recipe_name_field = config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip()
+ barcode_enable_field = config.get("recipe", {}).get("barcode_enable_field",
+ "verifica_codice_a_barre_abilitata").strip()
+ barcode_serial_field = config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip()
+ print_template_field = config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip()
+ data = []
+ fieldnames = set() # Use a set to avoid duplicates
+
+ # Iterate over all recipes in the database
+ for recipe in Recipes.select():
+ steps = recipe.get_steps_map()
+ exportable = {
+ # Base fields
+ recipe_name_field: recipe.name,
+ "cliente": recipe.client,
+ "part_number": recipe.part_number,
+ }
+
+ # Add base fields to the fieldnames
+ fieldnames.update([recipe_name_field, "cliente", "part_number"])
+
+ # Check and add steps conditionally
+ if "connector" in steps:
+ exportable.update({
+ "verifica_connettore_abilitata": "x",
+ "connettore": steps["connector"].spec["connector"]
+ })
+ fieldnames.update(["verifica_connettore_abilitata", "connettore"])
+
+ if "resistance" in steps:
+ exportable.update({
+ "verifica_resistenza_connettore_abilitata": "x",
+ "scala_resistenza": steps["resistance"].spec["scale"],
+ "r nominale": steps["resistance"].spec["expected"],
+ "tolleranza_resistenza_pos": steps["resistance"].spec["tolerance_pos"],
+ "tolleranza_resistenza_neg": steps["resistance"].spec["tolerance_neg"],
+ })
+ fieldnames.update(["verifica_resistenza_connettore_abilitata", "scala_resistenza", "r nominale",
+ "tolleranza_resistenza_pos", "tolleranza_resistenza_neg"])
+
+ if "barcodes" in steps:
+ exportable.update({
+ barcode_enable_field: "x",
+ barcode_serial_field: steps["barcodes"].spec["serial"]
+ })
+ fieldnames.update([barcode_enable_field, barcode_serial_field])
+
+ if "screws" in steps:
+ exportable.update({
+ "avvitatura_abilitata": "x",
+ "viti": steps["screws"].spec["quantity"]
+ })
+ fieldnames.update(["avvitatura_abilitata", "viti"])
+
+ if "leak_1" in steps:
+ exportable.update({
+ "prova_tenuta_abilitata": "x",
+ "tempo_pre_riempimento": steps["leak_1"].spec["pre_filling_time"],
+ "pressione_pre_riempimento": steps["leak_1"].spec["pre_filling_pressure"],
+ "tempo_di_test": steps["leak_1"].spec["test_time"],
+ "pressione_di_test": steps["leak_1"].spec["test_pressure"],
+ })
+ fieldnames.update(["prova_tenuta_abilitata", "tempo_pre_riempimento", "pressione_pre_riempimento",
+ "tempo_di_test", "pressione_di_test"])
+
+ if "leak_2" in steps:
+ exportable.update({
+ "prova_tenuta_abilitata_2": "x",
+ "tempo_pre_riempimento_2": steps["leak_2"].spec["pre_filling_time"],
+ "pressione_pre_riempimento_2": steps["leak_2"].spec["pre_filling_pressure"],
+ "tempo_di_test_2": steps["leak_2"].spec["test_time"],
+ "pressione_di_test_2": steps["leak_2"].spec["test_pressure"],
+ })
+ fieldnames.update(["prova_tenuta_abilitata_2", "tempo_pre_riempimento_2", "pressione_pre_riempimento_2",
+ "tempo_di_test_2", "pressione_di_test_2"])
+
+ if "vision" in steps:
+ exportable.update({
+ "test_visione_abilitato": steps["vision"].spec.get("enabled", ""),
+ "ricetta_visione": steps["vision"].spec["recipe"]
+ })
+ fieldnames.update(["test_visione_abilitato", "ricetta_visione"])
+
+ if "print" in steps:
+ exportable.update({
+ "stampa_etichetta_abilitata": "x",
+ print_template_field: steps["print"].spec["template"],
+ })
+ fieldnames.update(["stampa_etichetta_abilitata", print_template_field])
+
+ # Append the exportable row to the data
+ data.append(exportable)
+
+ # Export data to CSV if there is any data
+ if len(data):
+ if logger:
+ logger.info(f"Exporting recipes to {csv_path}")
+ with open(csv_path, "w", newline="") as f:
+ writer = csv.DictWriter(f, fieldnames=list(fieldnames))
+ writer.writeheader()
+ writer.writerows(data)
+ if logger:
+ logger.info(f"Exported {len(data)} recipes to {csv_path}.")
+
+
+def backup_current_recipes(config, logger=None):
+ """
+ Back up current recipes to a timestamped CSV file in the predefined backup directory.
+ """
+ # Define the backup directory and file name
+ backup_dir = os.path.join('config', 'csv_import', 'backup_csv')
+ timestamp = datetime.now().strftime("%d%m%y%H%M%S")
+ backup_file = f"backup_{timestamp}.csv"
+ backup_path = os.path.join(backup_dir, backup_file)
+
+ # Ensure the backup directory exists
+ os.makedirs(backup_dir, exist_ok=True)
+
+ # Export current recipes to the backup path
+ export_recipes(config=config, csv_path=backup_path, logger=logger)
+
+ if logger:
+ logger.info(f"Backup created at: {backup_path}")
+
+ return backup_path # Return the backup path for reference if needed
+
+
diff --git a/src/lib/nfc/__init__.py b/src/lib/nfc/__init__.py
new file mode 100644
index 0000000..6d0c47d
--- /dev/null
+++ b/src/lib/nfc/__init__.py
@@ -0,0 +1,47 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+from . import clf # noqa: F401
+from . import tag # noqa: F401
+from . import llcp # noqa: F401
+from . import snep # noqa: F401
+from . import handover # noqa: F401
+from .clf import ContactlessFrontend # noqa: F401
+
+import logging
+logging.getLogger(__name__).addHandler(logging.NullHandler())
+logging.getLogger(__name__).setLevel(logging.INFO)
+
+# METADATA ####################################################################
+
+__version__ = "1.0.4"
+
+__title__ = "nfcpy"
+__description__ = "Python module for Near Field Communication."
+__uri__ = "https://github.com/nfcpy/nfcpy"
+
+__author__ = "Stephen Tiedemann"
+__email__ = "stephen.tiedemann@gmail.com"
+
+__license__ = "EUPL"
+__copyright__ = "Copyright (c) 2009, 2019 Stephen Tiedemann"
+
+###############################################################################
diff --git a/src/lib/nfc/__main__.py b/src/lib/nfc/__main__.py
new file mode 100644
index 0000000..a5bc5a0
--- /dev/null
+++ b/src/lib/nfc/__main__.py
@@ -0,0 +1,214 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2016 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import nfc
+import nfc.clf.device
+import nfc.clf.transport
+
+import os
+import errno
+import logging
+import platform
+import argparse
+import subprocess
+
+description = """
+
+The nfcpy module implements a near field communication software stack
+for reading and writing NFC Tags or peer-to-peer communication with
+another NFC Device. It requires an NFC radio module connected through
+either USB or serial interface. The nfcpy module is supposed to be
+used within other applications, executing it as a module will try to
+locate contactless devices connected to this machine.
+
+"""
+
+
+def main(args):
+ print("This is the %s version of nfcpy run in Python %s\non %s" %
+ (nfc.__version__, platform.python_version(), platform.platform()))
+ print("I'm now searching your system for contactless devices")
+
+ logging.basicConfig()
+ log_levels = (logging.WARN, logging.INFO, logging.DEBUG, logging.DEBUG-1)
+ log_level = log_levels[min(args.verbose, len(log_levels) - 1)]
+ logging.getLogger('nfc').setLevel(log_level)
+
+ found = 0
+ for vid, pid, bus, dev in nfc.clf.transport.USB.find("usb"):
+ if (vid, pid) in nfc.clf.device.usb_device_map:
+ path = "usb:{0:03d}:{1:03d}".format(bus, dev)
+ try:
+ clf = nfc.ContactlessFrontend(path)
+ print("** found %s" % clf.device)
+ clf.close()
+ found += 1
+ except IOError as error:
+ if error.errno == errno.EACCES:
+ usb_device_access_denied(bus, dev, vid, pid, path)
+ elif error.errno == errno.EBUSY:
+ usb_device_found_is_busy(bus, dev, vid, pid, path)
+
+ if args.search_tty:
+ for dev in nfc.clf.transport.TTY.find("tty")[0]:
+ path = "tty:{0}".format(dev[8:])
+ try:
+ clf = nfc.ContactlessFrontend(path)
+ print("** found %s" % clf.device)
+ clf.close()
+ found += 1
+ except IOError as error:
+ if error.errno == errno.EACCES:
+ print("access denied for device with path %s" % path)
+ elif error.errno == errno.EBUSY:
+ print("the device with path %s is busy" % path)
+ else:
+ print("I'm not trying serial devices because you haven't told me")
+ print("-- add the option '--search-tty' to have me looking")
+ print("-- but beware that this may break other serial devs")
+
+ if not found:
+ print("Sorry, but I couldn't find any contactless device")
+
+
+def usb_device_access_denied(bus, dev, vid, pid, path):
+ info = "** found usb:{vid:04x}:{pid:04x} at {path} but access is denied"
+ print(info.format(vid=vid, pid=pid, path=path))
+ if platform.system().lower() == "linux":
+ devnode = "/dev/bus/usb/{0:03d}/{1:03d}".format(bus, dev)
+ if not os.access(devnode, os.R_OK | os.W_OK):
+ import pwd
+ import grp
+ usrname = pwd.getpwuid(os.getuid()).pw_name
+ devinfo = os.stat(devnode)
+ dev_usr = pwd.getpwuid(devinfo.st_uid).pw_name
+ dev_grp = grp.getgrgid(devinfo.st_gid).gr_name
+ try:
+ plugdev = grp.getgrnam("plugdev")
+ except KeyError:
+ plugdev = None
+
+ udev_rule = 'SUBSYSTEM==\\"usb\\", ACTION==\\"add\\", ' \
+ 'ATTRS{{idVendor}}==\\"{vid:04x}\\", ' \
+ 'ATTRS{{idProduct}}==\\"{pid:04x}\\", ' \
+ '{action}'
+ udev_file = "/etc/udev/rules.d/nfcdev.rules"
+
+ print("-- the device is owned by '{dev_usr}' but you are '{user}'"
+ .format(dev_usr=dev_usr, user=usrname))
+ print("-- also members of the '{dev_grp}' group would be permitted"
+ .format(dev_grp=dev_grp))
+ print("-- you could use 'sudo' but this is not recommended")
+
+ if plugdev is None:
+ print("-- it's better to adjust the device permissions")
+ action = 'MODE=\\"0666\\"'
+ udev_rule = udev_rule.format(vid=vid, pid=pid, action=action)
+ print(" sudo sh -c 'echo {udev_rule} >> {udev_file}'"
+ .format(udev_rule=udev_rule, udev_file=udev_file))
+ print(" sudo udevadm control -R # then re-attach device")
+ elif dev_grp != "plugdev":
+ print("-- better assign the device to the 'plugdev' group")
+ action = 'GROUP=\\"plugdev\\"'
+ udev_rule = udev_rule.format(vid=vid, pid=pid, action=action)
+ print(" sudo sh -c 'echo {udev_rule} >> {udev_file}'"
+ .format(udev_rule=udev_rule, udev_file=udev_file))
+ print(" sudo udevadm control -R # then re-attach device")
+ if usrname not in plugdev.gr_mem:
+ print("-- and make yourself member of the 'plugdev' group")
+ print(" sudo adduser {0} plugdev".format(usrname))
+ print(" su - {0} # or logout once".format(usrname))
+ elif usrname not in plugdev.gr_mem:
+ print("-- you should add yourself to the 'plugdev' group")
+ print(" sudo adduser {0} plugdev".format(usrname))
+ print(" su - {0} # or logout once".format(usrname))
+ else:
+ print("-- but unfortunately I have no better idea than that")
+
+
+def usb_device_found_is_busy(bus, dev, vid, pid, path):
+ info = "** found usb:{vid:04x}:{pid:04x} at {path} but it's already used"
+ print(info.format(vid=vid, pid=pid, path=path))
+ if platform.system().lower() == "linux":
+ sysfs = '/sys/bus/usb/devices/'
+ for entry in os.listdir(sysfs):
+ if not entry.startswith("usb") and ':' not in entry:
+ sysfs_device_entry = sysfs + entry + '/'
+ busnum = open(sysfs_device_entry + 'busnum').read().strip()
+ devnum = open(sysfs_device_entry + 'devnum').read().strip()
+ if int(busnum) == bus and int(devnum) == dev:
+ break
+ else:
+ print("-- impossible but nothing found in /sys/bus/usb/devices")
+ return
+
+ # We now have the sysfs entry for the device in question. All
+ # supported contactless devices have a single configuration
+ # that will be listed if the device is used by another driver.
+
+ blf = "/etc/modprobe.d/blacklist-nfc.conf"
+ sysfs_config_entry = sysfs_device_entry[:-1] + ":1.0/"
+ print("-- scan sysfs entry at '%s'" % sysfs_config_entry)
+ driver = os.readlink(sysfs_config_entry + "driver").split('/')[-1]
+ print("-- the device is used by the '%s' kernel driver" % driver)
+ if os.access(sysfs_config_entry + "nfc", os.F_OK):
+ print("-- this kernel driver belongs to the linux nfc subsystem")
+ print("-- you can remove it to free the device for this session")
+ print(" sudo modprobe -r %s" % driver)
+ print("-- and blacklist the driver to prevent loading next time")
+ print(" sudo sh -c 'echo blacklist %s >> %s'" % (driver, blf))
+ elif driver == "usbfs":
+ print("-- this indicates a user mode driver with libusb")
+ devnode = "/dev/bus/usb/{0:03d}/{1:03d}".format(bus, dev)
+ print("-- find the process that uses " + devnode)
+ try:
+ subprocess.check_output("which lsof".split())
+ except subprocess.CalledProcessError:
+ print("-- there is no 'lsof' command, can't help further")
+ else:
+ lsof = "lsof -t " + devnode
+ try:
+ pid = subprocess.check_output(lsof.split()).strip()
+ except subprocess.CalledProcessError:
+ pid = None
+ if pid is not None:
+ ps = "ps --no-headers -o cmd -p %s" % pid
+ cmd = subprocess.check_output(ps.split()).strip()
+ cwd = os.readlink("/proc/%s/cwd" % pid)
+ print("-- found that process %s uses the device" % pid)
+ print("-- process %s is '%s'" % (pid, cmd))
+ print("-- in directory '%s'" % cwd)
+ else:
+ print(" ps --no-headers -o cmd -p `sudo %s`" % lsof)
+
+
+parser = argparse.ArgumentParser(
+ prog="python -m nfc", description=description)
+
+parser.add_argument(
+ "--search-tty", action="store_true",
+ help="do also search for serial devices on linux")
+
+parser.add_argument(
+ "--verbose", "-v", action="count", default=0,
+ help="be verbose. Multiple -v options increase the verbosity.")
+
+main(parser.parse_args())
diff --git a/src/lib/nfc/clf/__init__.py b/src/lib/nfc/clf/__init__.py
new file mode 100644
index 0000000..c6154dc
--- /dev/null
+++ b/src/lib/nfc/clf/__init__.py
@@ -0,0 +1,1251 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+
+import src.lib.nfc.tag
+import src.lib.nfc.dep
+import src.lib.nfc.llcp
+from . import device
+
+import binascii
+import os
+import re
+import time
+import errno
+import threading
+
+import logging
+log = logging.getLogger(__name__)
+
+
+def print_data(data):
+ return 'None' if data is None else binascii.hexlify(data).decode('latin')
+
+
+class ContactlessFrontend(object):
+ """This class is the main interface for working with contactless
+ devices. The :meth:`connect` method provides easy access to the
+ contactless functionality through automated discovery of remote
+ cards and devices and activation of appropiate upper level
+ protocols for further interaction. The :meth:`sense`,
+ :meth:`listen` and :meth:`exchange` methods provide a low-level
+ interface for more specialized tasks.
+
+ An instance of the :class:`ContactlessFrontend` class manages a
+ single contactless device locally connect through either USB, TTY
+ or COM port. A special UDP port driver allows for emulation of a
+ contactless device that connects through UDP to another emulated
+ contactless device for test and development of higher layer
+ functions.
+
+ A locally connected contactless device can be opened by either
+ supplying a *path* argument when an an instance of the contactless
+ frontend class is created or by calling :meth:`open` at a later
+ time. In either case the *path* argument must be constructed as
+ described in :meth:`open` and the same exceptions may occur. The
+ difference is that :meth:`open` returns False if a device could
+ not be found whereas the initialization method raises
+ :exc:`~exceptions.IOError` with :data:`errno.ENODEV`.
+
+ The methods of the :class:`ContactlessFrontend` class are
+ thread-safe.
+
+ """
+ def __init__(self, path=None):
+ self.device = None
+ self.target = None
+ self.lock = threading.Lock()
+ if path and not self.open(path):
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ def open(self, path):
+ """Open a contactless reader identified by the search *path*.
+
+ The :meth:`open` method searches and then opens a contactless
+ reader device for further communication. The *path* argument
+ can be flexibly constructed to identify more or less precisely
+ the device to open. A *path* that only partially identifies a
+ device is completed by search. The first device that is found
+ and successfully opened causes :meth:`open` to return True. If
+ no device is found return value is False. If a device was
+ found but could not be opened then :meth:`open` returns False
+ if *path* was partial or raise :exc:`~exceptions.IOError` if
+ *path* was fully qualified. Typical I/O error reasons are
+ :data:`errno.EACCES` if the calling process has insufficient
+ access rights or :data:`errno.EBUSY` if the device is used by
+ another process.
+
+ A path is constructed as follows:
+
+ ``usb[:vendor[:product]]``
+
+ with optional *vendor* and *product* as four digit
+ hexadecimal numbers. For example, ``usb:054c:06c3`` would
+ open the first Sony RC-S380 reader while ``usb:054c`` would
+ open the first Sony reader found on USB.
+
+ ``usb[:bus[:device]]``
+
+ with optional *bus* and *device* number as three-digit
+ decimals. For example, ``usb:001:023`` would open the
+ device enumerated as number 23 on bus 1 while ``usb:001``
+ would open the first device found on bust 1. Note that a
+ new device number is generated every time the device is
+ plugged into USB. Bus and device numbers are shown by
+ ``lsusb``.
+
+ ``tty:port:driver``
+
+ with mandatory *port* and *driver* name. This is for Posix
+ systems to open the serial port ``/dev/tty`` and use
+ the driver module ``nfc/dev/.py`` for access. For
+ example, ``tty:USB0:arygon`` would open ``/dev/ttyUSB0``
+ and load the Arygon APPx/ADRx driver.
+
+ ``com:port:driver``
+
+ with mandatory *port* and *driver* name. This is for
+ Windows systems to open the serial port ``COM`` and
+ use the driver module ``nfc/dev/.py`` for access.
+
+ ``udp[:host][:port]``
+
+ with optional *host* name or address and *port*
+ number. This will emulate a communication channel over
+ UDP/IP. The defaults for *host* and *port* are
+ ``localhost:54321``.
+
+ """
+ if not isinstance(path, str):
+ raise TypeError("expecting a string type argument *path*")
+ if not len(path) > 0:
+ raise ValueError("argument *path* must not be empty")
+
+ # Close current device driver if this is not the first
+ # open. This allows to use several devices sequentially or
+ # re-initialize a device.
+ self.close()
+
+ # Acquire the lock and search for a device on *path*
+ with self.lock:
+ log.info("searching for reader on path " + path)
+ self.device = device.connect(path)
+ if self.device:
+ log.info("using {0}".format(self.device))
+ else:
+ log.error("no reader available on path " + path)
+ return bool(self.device)
+
+ def close(self):
+ """Close the contacless reader device."""
+ with self.lock:
+ if self.device is not None:
+ try:
+ self.device.close()
+ except IOError:
+ pass
+ self.device = None
+
+ def connect(self, **options):
+ """Connect with a Target or Initiator
+
+ The calling thread is blocked until a single activation and
+ deactivation has completed or a callback function supplied as
+ the keyword argument ``terminate`` returns a true value. The
+ example below makes :meth:`~connect()` return after 5 seconds,
+ regardless of whether a peer device was connected or not.
+
+ >>> import nfc, time
+ >>> clf = nfc.ContactlessFrontend('usb')
+ >>> after5s = lambda: time.time() - started > 5
+ >>> started = time.time(); clf.connect(llcp={}, terminate=after5s)
+
+ Connect options are given as keyword arguments with dictionary
+ values. Possible options are:
+
+ * ``rdwr={key: value, ...}`` - options for reader/writer
+ * ``llcp={key: value, ...}`` - options for peer to peer
+ * ``card={key: value, ...}`` - options for card emulation
+
+ **Reader/Writer Options**
+
+ 'targets' : iterable
+ A list of bitrate and technology type strings that will
+ produce the :class:`~nfc.clf.RemoteTarget` objects to
+ discover. The default is ``('106A', '106B', '212F')``.
+
+ 'on-startup' : function(targets)
+ This function is called before any attempt to discover a
+ remote card. The *targets* argument provides a list of
+ :class:`RemoteTarget` objects prepared from the 'targets'
+ bitrate and technology type strings. The function must
+ return a list of of those :class:`RemoteTarget` objects
+ that shall be finally used for discovery, those targets may
+ have additional attributes. An empty list or anything else
+ that evaluates false will remove the 'rdwr' option
+ completely.
+
+ 'on-discover' : function(target)
+ This function is called when a :class:`RemoteTarget` has
+ been discovered. The *target* argument contains the
+ technology type specific discovery responses and should be
+ evaluated for multi-protocol support. The target will be
+ further activated only if this function returns a true
+ value. The default function depends on the 'llcp' option,
+ if present then the function returns True only if the
+ target does not indicate peer to peer protocol support,
+ otherwise it returns True for all targets.
+
+ 'on-connect' : function(tag)
+ This function is called when a remote tag has been
+ activated. The *tag* argument is an instance of class
+ :class:`nfc.tag.Tag` and can be used for tag reading and
+ writing within the callback or in a separate thread. Any
+ true return value instructs :meth:`connect` to wait until
+ the tag is no longer present and then return True, any
+ false return value implies immediate return of the
+ :class:`nfc.tag.Tag` object.
+
+ 'on-release' : function(tag)
+ This function is called when the presence check was run
+ (the 'on-connect' function returned a true value) and
+ determined that communication with the *tag* has become
+ impossible, or when the 'terminate' function returned a
+ true value. The *tag* object may be used for cleanup
+ actions but not for communication.
+
+ 'iterations' : integer
+ This determines the number of sense cycles performed
+ between calls to the terminate function. Each iteration
+ searches once for all specified targets. The default value
+ is 5 iterations and between each iteration is a waiting
+ time determined by the 'interval' option described below.
+ As an effect of math there will be no waiting time if
+ iterations is set to 1.
+
+ 'interval' : float
+ This determines the waiting time between iterations. The
+ default value of 0.5 seconds is considered a sensible
+ tradeoff between responsiveness in terms of tag discovery
+ and power consumption. It should be clear that changing
+ this value will impair one or the other. There is no free
+ beer.
+
+ 'beep-on-connect': boolean
+ If the device supports beeping or flashing an LED,
+ automatically perform this functionality when a tag is
+ successfully detected AND the 'on-connect' function
+ returns a true value. Defaults to True.
+
+ .. sourcecode:: python
+
+ import nfc
+
+ def on_startup(targets):
+ for target in targets:
+ target.sensf_req = bytearray.fromhex("0012FC0000")
+ return targets
+
+ def on_connect(tag):
+ print(tag)
+
+ rdwr_options = {
+ 'targets': ['212F', '424F'],
+ 'on-startup': on_startup,
+ 'on-connect': on_connect,
+ }
+ with nfc.ContactlessFrontend('usb') as clf:
+ tag = clf.connect(rdwr=rdwr_options)
+ if tag.ndef:
+ print(tag.ndef.message.pretty())
+
+ **Peer To Peer Options**
+
+ 'on-startup' : function(llc)
+ This function is called before any attempt to establish
+ peer to peer communication. The *llc* argument provides the
+ :class:`~nfc.llcp.llc.LogicalLinkController` that may be
+ used to allocate and bind listen sockets for local
+ services. The function should return the *llc* object if
+ activation shall continue. Any other value removes the
+ 'llcp' option.
+
+ 'on-connect' : function(llc)
+ This function is called when peer to peer communication is
+ successfully established. The *llc* argument provides the
+ now activated :class:`~nfc.llcp.llc.LogicalLinkController`
+ ready for allocation of client communication sockets and
+ data exchange in separate work threads. The function should
+ a true value return more or less immediately, unless it
+ wishes to handle the logical link controller run loop by
+ itself and anytime later return a false value.
+
+ 'on-release' : function(llc)
+ This function is called when the symmetry loop was run (the
+ 'on-connect' function returned a true value) and determined
+ that communication with the remote peer has become
+ impossible, or when the 'terminate' function returned a
+ true value. The *llc* object may be used for cleanup
+ actions but not for communication.
+
+ 'role' : string
+ This attribute determines whether the local device will
+ restrict itself to either ``'initiator'`` or ``'target'``
+ mode of operation. As Initiator the local device will try
+ to discover a remote device. As Target it waits for being
+ discovered. The default is to alternate between both roles.
+
+ 'miu' : integer
+ This attribute sets the maximum information unit size that
+ is announced to the remote device during link activation.
+ The default and also smallest possible value is 128 bytes.
+
+ 'lto' : integer
+ This attribute sets the link timeout value (given in
+ milliseconds) that is announced to the remote device during
+ link activation. It informs the remote device that if the
+ local device does not return a protocol data unit before
+ the timeout expires, the communication link is broken and
+ can not be recovered. The *lto* is an important part of the
+ user experience, it ultimately tells when the user should
+ no longer expect communication to continue. The default
+ value is 500 millisecond.
+
+ 'agf' : boolean
+ Some early phone implementations did not properly handle
+ aggregated protocol data units. This attribute allows to
+ disable the use af aggregation at the cost of efficiency.
+ Aggregation is disabled with a false value. The default
+ is to use aggregation.
+
+ 'brs' : integer
+ For the local device in Initiator role the bit rate
+ selector determines the the bitrate to negotiate with the
+ remote Target. The value may be 0, 1, or 2 for 106, 212, or
+ 424 kbps, respectively. The default is to negotiate 424
+ kbps.
+
+ 'acm' : boolean
+ For the local device in Initiator role this attribute
+ determines whether a remote Target may also be activated in
+ active communication mode. In active communication mode
+ both peer devices mutually generate a radio field when
+ sending. The default is to use passive communication mode.
+
+ 'rwt' : float
+ For the local device in Target role this attribute sets the
+ response waiting time announced during link activation. The
+ response waiting time is a medium access layer (NFC-DEP)
+ value that indicates when the remote Initiator shall
+ attempt error recovery after missing a Target response. The
+ value is the waiting time index *wt* that determines the
+ effective response waiting time by the formula ``rwt =
+ 4096/13.56E6 * pow(2, wt)``. The value shall not be greater
+ than 14. The default value is 8 and yields an effective
+ response waiting time of 77.33 ms.
+
+ 'lri' : integer
+ For the local device in Initiator role this attribute sets
+ the length reduction for medium access layer (NFC-DEP)
+ information frames. The value may be 0, 1, 2, or 3 for a
+ maximum payload size of 64, 128, 192, or 254 bytes,
+ respectively. The default value is 3.
+
+ 'lrt' : integer
+ For the local device in Target role this attribute sets
+ the length reduction for medium access layer (NFC-DEP)
+ information frames. The value may be 0, 1, 2, or 3 for a
+ maximum payload size of 64, 128, 192, or 254 bytes,
+ respectively. The default value is 3.
+
+ .. sourcecode:: python
+
+ import nfc
+ import nfc.llcp
+ import threading
+
+ def server(socket):
+ message, address = socket.recvfrom()
+ socket.sendto("It's me!", address)
+ socket.close()
+
+ def client(socket):
+ socket.sendto("Hi there!", address=32)
+ socket.close()
+
+ def on_startup(llc):
+ socket = nfc.llcp.Socket(llc, nfc.llcp.LOGICAL_DATA_LINK)
+ socket.bind(address=32)
+ threading.Thread(target=server, args=(socket,)).start()
+ return llc
+
+ def on_connect(llc):
+ socket = nfc.llcp.Socket(llc, nfc.llcp.LOGICAL_DATA_LINK)
+ threading.Thread(target=client, args=(socket,)).start()
+ return True
+
+ llcp_options = {
+ 'on-startup': on_startup,
+ 'on-connect': on_connect,
+ }
+ with nfc.ContactlessFrontend('usb') as clf:
+ clf.connect(llcp=llcp_options)
+ print("link terminated")
+
+ **Card Emulation Options**
+
+ 'on-startup' : function(target)
+ This function is called to prepare a local target for
+ discovery. The input argument is a fresh instance of an
+ unspecific :class:`LocalTarget` that can be set to the
+ desired bitrate and modulation type and populated with the
+ type specific discovery responses (see :meth:`listen` for
+ response data that is needed). The fully specified target
+ object must then be returned.
+
+ 'on-discover' : function(target)
+ This function is called when the :class:`LocalTarget` has
+ been discovered. The *target* argument contains the
+ technology type specific discovery commands. The target
+ will be further activated only if this function returns a
+ true value. The default function always returns True.
+
+ 'on-connect' : function(tag)
+ This function is called when the local target was
+ discovered and a :class:`nfc.tag.TagEmulation` object
+ successfully initialized. The function receives the
+ emulated *tag* object which stores the first command
+ received after inialization as ``tag.cmd``. The function
+ should return a true value if the tag.process_command() and
+ tag.send_response() methods shall be called repeatedly
+ until either the remote device terminates communication or
+ the 'terminate' function returns a true value. The function
+ should return a false value if the :meth:`connect` method
+ shall return immediately with the emulated *tag* object.
+
+ 'on-release' : function(tag)
+ This function is called when the Target was released by the
+ Initiator or simply moved away, or if the terminate
+ callback function has returned a true value. The emulated
+ *tag* object may be used for cleanup actions but not for
+ communication.
+
+ .. sourcecode:: python
+
+ import nfc
+
+ def on_startup(target):
+ idm = bytearray.fromhex("01010501b00ac30b")
+ pmm = bytearray.fromhex("03014b024f4993ff")
+ sys = bytearray.fromhex("1234")
+ target.brty = "212F"
+ target.sensf_res = chr(1) + idm + pmm + sys
+ return target
+
+ def on_connect(tag):
+ print("discovered by remote reader")
+ return True
+
+ def on_release(tag):
+ print("remote reader is gone")
+ return True
+
+ card_options = {
+ 'on-startup': on_startup,
+ 'on-connect': on_connect,
+ 'on-release': on_release,
+ }
+ with nfc.ContactlessFrontend('usb') as clf:
+ clf.connect(card=card_options)
+
+ **Return Value**
+
+ The :meth:`connect` method returns :const:`None` if there were
+ no options left after the 'on-startup' functions have been
+ executed or when the 'terminate' function returned a true
+ value. It returns :const:`False` when terminated by any of the
+ following exceptions: :exc:`~exceptions.KeyboardInterrupt`,
+ :exc:`~exceptions.IOError`, :exc:`UnsupportedTargetError`.
+
+ The :meth:`connect` method returns a :class:`~nfc.tag.Tag`,
+ :class:`~nfc.llcp.llc.LogicalLinkController`, or
+ :class:`~nfc.tag.TagEmulation` object if the associated
+ 'on-connect' function returned a false value to indicate that
+ it will handle presence check, peer to peer symmetry loop, or
+ command/response processing by itself.
+
+ """
+ if self.device is None:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ log.debug("connect{0}".format(
+ tuple([k for k in options if options[k]])))
+
+ terminate = options.get('terminate', lambda: False)
+ rdwr_options = options.get('rdwr')
+ llcp_options = options.get('llcp')
+ card_options = options.get('card')
+
+ try:
+ assert isinstance(rdwr_options, (dict, type(None))), "rdwr"
+ assert isinstance(llcp_options, (dict, type(None))), "llcp"
+ assert isinstance(card_options, (dict, type(None))), "card"
+ except AssertionError as error:
+ raise TypeError("argument '%s' must be a dictionary" % error)
+
+ if llcp_options is not None:
+ llcp_options = dict(llcp_options)
+ llcp_options.setdefault('on-startup', lambda llc: llc)
+ llcp_options.setdefault('on-connect', lambda llc: True)
+ llcp_options.setdefault('on-release', lambda llc: True)
+
+ llc = nfc.llcp.llc.LogicalLinkController(**llcp_options)
+ llc = llcp_options['on-startup'](llc)
+ if isinstance(llc, nfc.llcp.llc.LogicalLinkController):
+ llcp_options['llc'] = llc
+ else:
+ log.debug("removing llcp_options after on-startup")
+ llcp_options = None
+
+ if rdwr_options is not None:
+ def on_discover(target):
+ if target.sel_res and target.sel_res[0] & 0x40:
+ return False
+ elif target.sensf_res and target.sensf_res[1:3] == b"\x01\xFE":
+ return False
+ else:
+ return True
+
+ rdwr_options = dict(rdwr_options)
+ rdwr_options.setdefault('targets', ['106A', '106B', '212F'])
+ rdwr_options.setdefault('on-startup', lambda targets: targets)
+ rdwr_options.setdefault('on-discover', on_discover)
+ rdwr_options.setdefault('on-connect', lambda tag: True)
+ rdwr_options.setdefault('on-release', lambda tag: True)
+ rdwr_options.setdefault('iterations', 5)
+ rdwr_options.setdefault('interval', 0.5)
+ rdwr_options.setdefault('beep-on-connect', True)
+
+ targets = [RemoteTarget(brty) for brty in rdwr_options['targets']]
+ targets = rdwr_options['on-startup'](targets)
+ if targets and all([isinstance(o, RemoteTarget) for o in targets]):
+ rdwr_options['targets'] = targets
+ else:
+ log.debug("removing rdwr_options after on-startup")
+ rdwr_options = None
+
+ if card_options is not None:
+ card_options = dict(card_options)
+ card_options.setdefault('on-startup', lambda target: None)
+ card_options.setdefault('on-discover', lambda target: True)
+ card_options.setdefault('on-connect', lambda tag: True)
+ card_options.setdefault('on-release', lambda tag: True)
+
+ target = nfc.clf.LocalTarget()
+ target = card_options['on-startup'](target)
+ if isinstance(target, LocalTarget):
+ card_options['target'] = target
+ else:
+ log.debug("removing card_options after on-startup")
+ card_options = None
+
+ if not (rdwr_options or llcp_options or card_options):
+ log.warning("no options to connect")
+ return None
+
+ log.debug("connect options after startup: %s",
+ ', '.join(filter(bool, ["rdwr" if rdwr_options else None,
+ "llcp" if llcp_options else None,
+ "card" if card_options else None])))
+
+ try:
+ while not terminate():
+ if rdwr_options:
+ result = self._rdwr_connect(rdwr_options, terminate)
+ if bool(result) is True:
+ return result
+ if llcp_options:
+ result = self._llcp_connect(llcp_options, terminate)
+ if bool(result) is True:
+ return result
+ if card_options:
+ result = self._card_connect(card_options, terminate)
+ if bool(result) is True:
+ return result
+ except IOError as error:
+ log.error(error)
+ return False
+ except UnsupportedTargetError as error:
+ log.info(error)
+ return False
+ except KeyboardInterrupt:
+ log.debug("terminated by keyboard interrupt")
+ return False
+
+ def _rdwr_connect(self, options, terminate):
+ target = self.sense(*options['targets'],
+ iterations=options['iterations'],
+ interval=options['interval'])
+ if target is not None:
+ log.debug("discovered target {0}".format(target))
+ if options['on-discover'](target):
+ tag = nfc.tag.activate(self, target)
+ if tag is not None:
+ log.debug("connected to {0}".format(tag))
+ if options['on-connect'](tag):
+ if options['beep-on-connect']:
+ self.device.turn_on_led_and_buzzer()
+ while not terminate() and tag.is_present:
+ time.sleep(0.1)
+ self.device.turn_off_led_and_buzzer()
+ return options['on-release'](tag)
+ else:
+ return tag
+
+ def _llcp_connect(self, options, terminate):
+ llc = options['llc']
+ for role in ('target', 'initiator'):
+ if options.get('role') is None or options.get('role') == role:
+ DEP = eval("nfc.dep." + role.capitalize())
+ dep_cfg = ('brs', 'acm', 'rwt', 'lrt', 'lri')
+ dep_cfg = {k: options[k] for k in dep_cfg if k in options}
+ if llc.activate(mac=DEP(clf=self), **dep_cfg):
+ log.debug("connected {0}".format(llc))
+ if options['on-connect'](llc):
+ llc.run(terminate=terminate)
+ return options['on-release'](llc)
+ else:
+ return llc
+
+ def _card_connect(self, options, terminate):
+ timeout = options.get('timeout', 1.0)
+ target = self.listen(options['target'], timeout)
+ if target and options['on-discover'](target):
+ log.debug("activated as {0}".format(target))
+ tag = nfc.tag.emulate(self, target)
+ if isinstance(tag, nfc.tag.TagEmulation):
+ log.debug("connected as {0}".format(tag))
+ if options['on-connect'](tag):
+ tag_rsp = tag.process_command(tag.cmd)
+ while not terminate():
+ try:
+ tag_cmd = tag.send_response(tag_rsp, None)
+ tag_rsp = tag.process_command(tag_cmd)
+ except nfc.clf.BrokenLinkError as error:
+ log.debug(error)
+ break
+ except nfc.clf.CommunicationError as error:
+ log.debug(error)
+ tag_rsp = None
+ return options['on-release'](tag)
+ else:
+ return tag
+
+ def sense(self, *targets, **options):
+ """Discover a contactless card or listening device.
+
+ .. note:: The :meth:`sense` method is intended for experts
+ with a good understanding of the commands and
+ responses exchanged during target activation (the
+ notion used for commands and responses follows the
+ NFC Forum Digital Specification). If the greater
+ level of control is not needed it is recommended to
+ use the :meth:`connect` method.
+
+ All positional arguments build the list of potential *targets*
+ to discover and must be of type :class:`RemoteTarget`. Keyword
+ argument *options* may be the number of ``iterations`` of the
+ sense loop set by *targets* and the ``interval`` between
+ iterations. The return value is either a :class:`RemoteTarget`
+ instance or :const:`None`.
+
+ >>> import nfc, nfc.clf
+ >>> from binascii import hexlify
+ >>> clf = nfc.ContactlessFrontend("usb")
+ >>> target1 = nfc.clf.RemoteTarget("106A")
+ >>> target2 = nfc.clf.RemoteTarget("212F")
+ >>> print(clf.sense(target1, target2, iterations=5, interval=0.2))
+ 106A(sdd_res=04497622D93881, sel_res=00, sens_res=4400)
+
+ A **Type A Target** is specified with the technology letter
+ ``A`` following the bitrate to be used for the SENS_REQ
+ command (almost always must the bitrate be 106 kbps). To
+ discover only a specific Type A target, the NFCID1 (UID) can
+ be set with a 4, 7, or 10 byte ``sel_req`` attribute (cascade
+ tags are handled internally).
+
+ >>> target = nfc.clf.RemoteTarget("106A")
+ >>> print(clf.sense(target))
+ 106A sdd_res=04497622D93881 sel_res=00 sens_res=4400
+ >>> target.sel_req = bytearray.fromhex("04497622D93881")
+ >>> print(clf.sense(target))
+ 106A sdd_res=04497622D93881 sel_res=00 sens_res=4400
+ >>> target.sel_req = bytearray.fromhex("04497622")
+ >>> print(clf.sense(target))
+ None
+
+ A **Type B Target** is specified with the technology letter
+ ``B`` following the bitrate to be used for the SENSB_REQ
+ command (almost always must the bitrate be 106 kbps). A
+ specific application family identifier can be set with the
+ first byte of a ``sensb_req`` attribute (the second byte PARAM
+ is ignored when it can not be set to local device, 00h is a
+ safe value in all cases).
+
+ >>> target = nfc.clf.RemoteTarget("106B")
+ >>> print(clf.sense(target))
+ 106B sens_res=50E5DD3DC900000011008185
+ >>> target.sensb_req = bytearray.fromhex("0000")
+ >>> print(clf.sense(target))
+ 106B sens_res=50E5DD3DC900000011008185
+ >>> target.sensb_req = bytearray.fromhex("FF00")
+ >>> print(clf.sense(target))
+ None
+
+ A **Type F Target** is specified with the technology letter
+ ``F`` following the bitrate to be used for the SENSF_REQ
+ command (the typically supported bitrates are 212 and 424
+ kbps). The default SENSF_REQ command allows all targets to
+ answer, requests system code information, and selects a single
+ time slot for the SENSF_RES response. This can be changed with
+ the ``sensf_req`` attribute.
+
+ >>> target = nfc.clf.RemoteTarget("212F")
+ >>> print(clf.sense(target))
+ 212F sensf_res=0101010601B00ADE0B03014B024F4993FF12FC
+ >>> target.sensf_req = bytearray.fromhex("0012FC0000")
+ >>> print(clf.sense(target))
+ 212F sensf_res=0101010601B00ADE0B03014B024F4993FF
+ >>> target.sensf_req = bytearray.fromhex("00ABCD0000")
+ >>> print(clf.sense(target))
+ None
+
+ An **Active Communication Mode P2P Target** search is selected
+ with an ``atr_req`` attribute. The choice of bitrate and
+ modulation type is 106A, 212F, and 424F.
+
+ >>> atr = bytearray.fromhex("D4000102030405060708091000000030")
+ >>> target = clf.sense(nfc.clf.RemoteTarget("106A", atr_req=atr))
+ >>> if target and target.atr_res:
+ >>> print(hexlify(target.atr_res).decode())
+ d501c023cae6b3182afe3dee0000000e3246666d01011103020013040196
+ >>> target = clf.sense(nfc.clf.RemoteTarget("424F", atr_req=atr))
+ >>> if target and target.atr_res:
+ >>> print(hexlify(target.atr_res).decode())
+ d501dc0104f04584e15769700000000e3246666d01011103020013040196
+
+ Some drivers must modify the ATR_REQ to cope with hardware
+ limitations, for example change length reduction value to
+ reduce the maximum size of target responses. The ATR_REQ that
+ has been send is given by the ``atr_req`` attribute of the
+ returned RemoteTarget object.
+
+ A **Passive Communication Mode P2P Target** responds to 106A
+ discovery with bit 6 of SEL_RES set to 1, and to 212F/424F
+ discovery (when the request code RC is 0 in the SENSF_REQ
+ command) with an NFCID2 that starts with 01FEh in the
+ SENSF_RES response. Responses below are from a Nexus 5
+ configured for NFC-DEP Protocol (SEL_RES bit 6 is set) and
+ Type 4A Tag (SEL_RES bit 5 is set).
+
+ >>> print(clf.sense(nfc.clf.RemoteTarget("106A")))
+ 106A sdd_res=08796BEB sel_res=60 sens_res=0400
+ >>> sensf_req = bytearray.fromhex("00FFFF0000")
+ >>> print(clf.sense(nfc.clf.RemoteTarget("424F", sensf_req=sensf_req)))
+ 424F sensf_res=0101FE1444EFB88FD50000000000000000
+
+ Errors found in the *targets* argument list raise exceptions
+ only if exactly one target is given. If multiple targets are
+ provided, any target that is not supported or has invalid
+ attributes is just ignored (but is logged as a debug message).
+
+ **Exceptions**
+
+ * :exc:`~exceptions.IOError` (ENODEV) when a local contacless
+ communication device has not been opened or communication
+ with the local device is no longer possible.
+
+ * :exc:`nfc.clf.UnsupportedTargetError` if the single target
+ supplied as input is not supported by the active driver.
+ This exception is never raised when :meth:`sense` is called
+ with multiple targets, those unsupported are then silently
+ ignored.
+
+ """
+ def sense_tta(target):
+ if target.sel_req and len(target.sel_req) not in (4, 7, 10):
+ raise ValueError("sel_req must be 4, 7, or 10 byte")
+ target = self.device.sense_tta(target)
+ log.debug("found %s", target)
+ if target and len(target.sens_res) != 2:
+ error = "SENS Response Format Error (wrong length)"
+ log.debug(error)
+ raise ProtocolError(error)
+ if target and target.sens_res[0] & 0b00011111 == 0:
+ if target.sens_res[1] & 0b00001111 != 0b1100:
+ error = "SENS Response Data Error (T1T config)"
+ log.debug(error)
+ raise ProtocolError(error)
+ if not target.rid_res:
+ error = "RID Response Error (no response received)"
+ log.debug(error)
+ raise ProtocolError(error)
+ if len(target.rid_res) != 6:
+ error = "RID Response Format Error (wrong length)"
+ log.debug(error)
+ raise ProtocolError(error)
+ if target.rid_res[0] >> 4 != 0b0001:
+ error = "RID Response Data Error (invalid HR0)"
+ log.debug(error)
+ raise ProtocolError(error)
+ return target
+
+ def sense_ttb(target):
+ return self.device.sense_ttb(target)
+
+ def sense_ttf(target):
+ return self.device.sense_ttf(target)
+
+ def sense_dep(target):
+ if len(target.atr_req) < 16:
+ raise ValueError("minimum atr_req length is 16 byte")
+ if len(target.atr_req) > 64:
+ raise ValueError("maximum atr_req length is 64 byte")
+ return self.device.sense_dep(target)
+
+ for target in targets:
+ if not isinstance(target, RemoteTarget):
+ raise ValueError("invalid target argument type: %r" % target)
+
+ with self.lock:
+ if self.device is None:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ self.target = None # forget captured target
+ self.device.mute() # deactivate the rf field
+
+ for i in range(max(1, options.get('iterations', 1))):
+ started = time.time()
+ for target in targets:
+ log.debug("sense {0}".format(target))
+ try:
+ if target.atr_req is not None:
+ self.target = sense_dep(target)
+ elif target.brty.endswith('A'):
+ self.target = sense_tta(target)
+ elif target.brty.endswith('B'):
+ self.target = sense_ttb(target)
+ elif target.brty.endswith('F'):
+ self.target = sense_ttf(target)
+ else:
+ info = "unknown technology type in %r"
+ raise UnsupportedTargetError(info % target.brty)
+ except UnsupportedTargetError as error:
+ if len(targets) == 1:
+ raise error
+ else:
+ log.debug(error)
+ except CommunicationError as error:
+ log.debug(error)
+ else:
+ if self.target is not None:
+ log.debug("found {0}".format(self.target))
+ return self.target
+ if len(targets) > 0:
+ self.device.mute() # deactivate the rf field
+ if i < options.get('iterations', 1) - 1:
+ elapsed = time.time() - started
+ time.sleep(max(0, options.get('interval', 0.1)-elapsed))
+
+ def listen(self, target, timeout):
+ """Listen *timeout* seconds to become activated as *target*.
+
+ .. note:: The :meth:`listen` method is intended for experts
+ with a good understanding of the commands and
+ responses exchanged during target activation (the
+ notion used for commands and responses follows the
+ NFC Forum Digital Specification). If the greater
+ level of control is not needed it is recommended to
+ use the :meth:`connect` method.
+
+ The *target* argument is a :class:`LocalTarget` object that
+ provides bitrate, technology type and response data
+ attributes. The return value is either a :class:`LocalTarget`
+ object with bitrate, technology type and request/response data
+ attributes or :const:`None`.
+
+ An **P2P Target** is selected when the ``atr_res`` attribute
+ is set. The bitrate and technology type are decided by the
+ Initiator and do not need to be specified. The ``sens_res``,
+ ``sdd_res`` and ``sel_res`` attributes for Type A technology
+ as well as the ``sensf_res`` attribute for Type F technolgy
+ must all be set.
+
+ When activated, the bitrate and type are set to the current
+ communication values, the ``atr_req`` attribute contains the
+ ATR_REQ received from the Initiator and the ``dep_req``
+ attribute contains the first DEP_REQ received after
+ activation. If the Initiator has changed communication
+ parameters, the ``psl_req`` attribute holds the PSL_REQ that
+ was received. The ``atr_res`` (and the ``psl_res`` if
+ transmitted) are also made available.
+
+ If the local target was activated in passive communication
+ mode either the Type A response (``sens_res``, ``sdd_res``,
+ ``sel_res``) or Type F response (``sensf_res``) attributes
+ will be present.
+
+ With a Nexus 5 on a reader connected via USB the following
+ code should be working and produce similar output (the Nexus 5
+ prioritizes active communication mode):
+
+ >>> import nfc, nfc.clf
+ >>> clf = nfc.ContactlessFrontend("usb")
+ >>> atr_res = "d50101fe0102030405060708000000083246666d010110"
+ >>> target = nfc.clf.LocalTarget()
+ >>> target.sensf_res = bytearray.fromhex("0101FE"+16*"FF")
+ >>> target.sens_res = bytearray.fromhex("0101")
+ >>> target.sdd_res = bytearray.fromhex("08010203")
+ >>> target.sel_res = bytearray.fromhex("40")
+ >>> target.atr_res = bytearray.fromhex(atr_res)
+ >>> print(clf.listen(target, timeout=2.5))
+ 424F atr_res=D50101FE0102030405060708000000083246666D010110 ...
+
+ A **Type A Target** is selected when ``atr_res`` is not
+ present and the technology type is ``A``. The bitrate should
+ be set to 106 kbps, even if a driver supports higher bitrates
+ they would need to be set after activation. The ``sens_res``,
+ ``sdd_res`` and ``sel_res`` attributes must all be provided.
+
+ >>> target = nfc.clf.Localtarget("106A")
+ >>> target.sens_res = bytearray.fromhex("0101"))
+ >>> target.sdd_res = bytearray.fromhex("08010203")
+ >>> target.sel_res = bytearray.fromhex("00")
+ >>> print(clf.listen(target, timeout=2.5))
+ 106A sdd_res=08010203 sel_res=00 sens_res=0101 tt2_cmd=3000
+
+ A **Type B Target** is selected when ``atr_res`` is not
+ present and the technology type is ``B``. Unfortunately none
+ of the supported devices supports Type B technology for listen
+ and an :exc:`nfc.clf.UnsupportedTargetError` exception will be
+ the only result.
+
+ >>> target = nfc.clf.LocalTarget("106B")
+ >>> try: clf.listen(target, 2.5)
+ ... except nfc.clf.UnsupportedTargetError: print("sorry")
+ ...
+ sorry
+
+ A **Type F Target** is selected when ``atr_res`` is not
+ present and the technology type is ``F``. The bitrate may be
+ 212 or 424 kbps. The ``sensf_res`` attribute must be provided.
+
+ >>> idm, pmm, sys = "02FE010203040506", "FFFFFFFFFFFFFFFF", "12FC"
+ >>> target = nfc.clf.LocalTarget("212F")
+ >>> target.sensf_res = bytearray.fromhex("01" + idm + pmm + sys)
+ >>> print(clf.listen(target, 2.5))
+ 212F sensf_req=00FFFF0003 tt3_cmd=0C02FE010203040506 ...
+
+ **Exceptions**
+
+ * :exc:`~exceptions.IOError` (ENODEV) when a local contacless
+ communication device has not been opened or communication
+ with the local device is no longer possible.
+
+ * :exc:`nfc.clf.UnsupportedTargetError` if the single target
+ supplied as input is not supported by the active driver.
+ This exception is never raised when :meth:`sense` is called
+ with multiple targets, those unsupported are then silently
+ ignored.
+
+ """
+ def listen_tta(target, timeout):
+ return self.device.listen_tta(target, timeout)
+
+ def listen_ttb(target, timeout):
+ return self.device.listen_ttb(target, timeout)
+
+ def listen_ttf(target, timeout):
+ return self.device.listen_ttf(target, timeout)
+
+ def listen_dep(target, timeout):
+ target = self.device.listen_dep(target, timeout)
+ if target and target.atr_req:
+ try:
+ assert len(target.atr_req) >= 16, "less than 16 byte"
+ assert len(target.atr_req) <= 64, "more than 64 byte"
+ return target
+ except AssertionError as error:
+ log.debug("atr_req is %s", str(error))
+
+ assert isinstance(target, LocalTarget), \
+ "invalid target argument type: %r" % target
+
+ with self.lock:
+ if self.device is None:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ self.target = None # forget captured target
+ self.device.mute() # deactivate the rf field
+
+ info = "listen %.3f seconds for %s"
+ if target.atr_res is not None:
+ log.debug(info, timeout, "DEP")
+ self.target = listen_dep(target, timeout)
+ elif target.brty in ('106A', '212A', '424A'):
+ log.debug(info, timeout, target)
+ self.target = listen_tta(target, timeout)
+ elif target.brty in ('106B', '212B', '424B', '848B'):
+ log.debug(info, timeout, target)
+ self.target = listen_ttb(target, timeout)
+ elif target.brty in ('212F', '424F'):
+ log.debug(info, timeout, target)
+ self.target = listen_ttf(target, timeout)
+ else:
+ errmsg = "unsupported bitrate technology type {}"
+ raise ValueError(errmsg.format(target.brty))
+
+ return self.target
+
+ def exchange(self, send_data, timeout):
+ """Exchange data with an activated target (*send_data* is a command
+ frame) or as an activated target (*send_data* is a response
+ frame). Returns a target response frame (if data is send to an
+ activated target) or a next command frame (if data is send
+ from an activated target). Returns None if the communication
+ link broke during exchange (if data is sent as a target). The
+ timeout is the number of seconds to wait for data to return,
+ if the timeout expires an nfc.clf.TimeoutException is
+ raised. Other nfc.clf.CommunicationError exceptions may be raised if
+ an error is detected during communication.
+
+ """
+ with self.lock:
+ if self.device is None:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ log.debug(">>> %s timeout=%s", print_data(send_data), str(timeout))
+
+ if isinstance(self.target, RemoteTarget):
+ exchange = self.device.send_cmd_recv_rsp
+ elif isinstance(self.target, LocalTarget):
+ exchange = self.device.send_rsp_recv_cmd
+ else:
+ log.error("no target for data exchange")
+ return None
+
+ send_time = time.time()
+ rcvd_data = exchange(self.target, send_data, timeout)
+ recv_time = time.time() - send_time
+
+ log.debug("<<< %s %.3fs", print_data(rcvd_data), recv_time)
+ return rcvd_data
+
+ @property
+ def max_send_data_size(self):
+ """The maximum number of octets that can be send with the
+ :meth:`exchange` method in the established operating mode.
+
+ """
+ with self.lock:
+ if self.device is None:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+ else:
+ return self.device.get_max_send_data_size(self.target)
+
+ @property
+ def max_recv_data_size(self):
+ """The maximum number of octets that can be received with the
+ :meth:`exchange` method in the established operating mode.
+
+ """
+ with self.lock:
+ if self.device is None:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+ else:
+ return self.device.get_max_recv_data_size(self.target)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
+ def __str__(self):
+ if self.device is not None:
+ s = "{dev.vendor_name} {dev.product_name} on {dev.path}"
+ return s.format(dev=self.device)
+ else:
+ return self.__repr__()
+
+
+###############################################################################
+#
+# Targets
+#
+###############################################################################
+class Target(object):
+ def __init__(self, **kwargs):
+ for name in kwargs:
+ self.__dict__[name] = kwargs[name]
+
+ def __getattr__(self, name):
+ return None
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ def __str__(self):
+ attrs = []
+ for name in sorted(self.__dict__.keys()):
+ if name.startswith('_'):
+ continue
+ value = self.__dict__[name]
+ if isinstance(value, (bytes, bytearray)):
+ value = binascii.hexlify(value).decode().upper()
+ attrs.append("{0}={1}".format(name, value))
+ return "{brty} {attrs}".format(brty=self.brty, attrs=' '.join(attrs))
+
+
+class RemoteTarget(Target):
+ """A RemoteTarget instance provides bitrate and technology type and
+ command/response data of a remote card or device that, when input
+ to :meth:`sense`, shall be attempted to discover and, when
+ returned by :meth:`sense`, has been discovered by the local
+ device. Command/response data attributes, whatever name, default
+ to None.
+
+ """
+ brty_pattern = re.compile(r'(\d+[A-Z])(?:/(\d+[A-Z])|.*)')
+
+ def __init__(self, brty, **kwargs):
+ super(RemoteTarget, self).__init__(**kwargs)
+ self.brty = brty
+
+ @property
+ def brty(self):
+ """A string that combines bitrate and technology type, e.g. '106A'."""
+ return self._brty_send
+
+ @brty.setter
+ def brty(self, value):
+ brty_pattern_match = self.brty_pattern.match(value)
+ if brty_pattern_match:
+ (self._brty_send, self._brty_recv) = brty_pattern_match.groups()
+ if not self._brty_recv:
+ self._brty_recv = self._brty_send
+ else:
+ raise ValueError("brty pattern does not match for %r" % value)
+
+ @property
+ def brty_send(self):
+ return self._brty_send
+
+ @property
+ def brty_recv(self):
+ return self._brty_recv
+
+
+class LocalTarget(Target):
+ """A LocalTarget instance provides bitrate and technology type and
+ command/response data of the local card or device that, when input
+ to :meth:`listen`, shall be made available for discovery and, when
+ returned by :meth:`listen`, has been discovered by a remote
+ device. Command/response data attributes, whatever name, default
+ to None.
+
+ """
+ def __init__(self, brty='106A', **kwargs):
+ super(LocalTarget, self).__init__(**kwargs)
+ self.brty = brty
+
+ @property
+ def brty(self):
+ """A string that combines bitrate and technology type, e.g. '106A'."""
+ return self._brty_send \
+ if self._brty_send == self._brty_recv \
+ else self._brty_send+"/"+self._brty_recv
+
+ @brty.setter
+ def brty(self, value):
+ self._brty_send = self._brty_recv = value
+
+
+###############################################################################
+#
+# Exceptions
+#
+###############################################################################
+class Error(Exception):
+ """Base class for exceptions specific to the contacless frontend module.
+
+ - UnsupportedTargetError
+ - CommunicationError
+
+ - ProtocolError
+ - TransmissionError
+ - TimeoutError
+ - BrokenLinkError
+
+ """
+
+
+class UnsupportedTargetError(Error):
+ """The :class:`RemoteTarget` input to
+ :meth:`ContactlessFrontend.sense` or :class:`LocalTarget` input to
+ :meth:`ContactlessFrontend.listen` is not supported by the local
+ device.
+
+ """
+
+
+class CommunicationError(Error):
+ """Base class for communication errors.
+
+ """
+
+
+class ProtocolError(CommunicationError):
+ """Raised when an NFC Forum Digital Specification protocol error
+ occured.
+
+ """
+
+
+class TransmissionError(CommunicationError):
+ """Raised when an NFC Forum Digital Specification transmission error
+ occured.
+
+ """
+
+
+class TimeoutError(CommunicationError):
+ """Raised when an NFC Forum Digital Specification timeout error
+ occured.
+
+ """
+
+
+class BrokenLinkError(CommunicationError):
+ """The remote device (Reader/Writer or P2P Device) has deactivated the
+ RF field or is no longer within communication distance.
+
+ """
diff --git a/src/lib/nfc/clf/acr122.py b/src/lib/nfc/clf/acr122.py
new file mode 100644
index 0000000..54b647b
--- /dev/null
+++ b/src/lib/nfc/clf/acr122.py
@@ -0,0 +1,242 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2011, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""Device driver for the Arygon ACR122U contactless reader.
+
+The Arygon ACR122U is a PC/SC compliant contactless reader that
+connects via USB and uses the USB CCID profile. It is normally
+intented to be used with a PC/SC stack but this driver interfaces
+directly with the inbuilt PN532 chipset by tunneling commands through
+the PC/SC Escape command. The driver is limited in functionality
+because the embedded microprocessor (that implements the PC/SC stack)
+also operates the PN532; it does not allow all commands to pass as
+desired and reacts on chip responses with its own (legitimate)
+interpretation of state.
+
+========== ======= ============
+function support remarks
+========== ======= ============
+sense_tta yes Type 1 (Topaz) Tags are not supported
+sense_ttb yes ATTRIB by firmware voided with S(DESELECT)
+sense_ttf yes
+sense_dep yes
+listen_tta no
+listen_ttb no
+listen_ttf no
+listen_dep no
+========== ======= ============
+
+"""
+import nfc.clf
+from . import pn532
+
+import os
+import errno
+import struct
+from binascii import hexlify
+
+import logging
+log = logging.getLogger(__name__)
+
+
+def init(transport):
+ device = Device(Chipset(transport))
+ device._vendor_name = transport.manufacturer_name
+ device._device_name = transport.product_name.split()[0]
+ return device
+
+
+class Device(pn532.Device):
+ # Device driver class for the ACR122U.
+
+ def __init__(self, chipset):
+ super(Device, self).__init__(chipset, logger=log)
+
+ def sense_tta(self, target):
+ """Activate the RF field and probe for a Type A Target at 106
+ kbps. Other bitrates are not supported. Type 1 Tags are not
+ supported because the device does not allow to send the
+ correct RID command (even though the PN532 does).
+
+ """
+ return super(Device, self).sense_tta(target)
+
+ def sense_ttb(self, target):
+ """Activate the RF field and probe for a Type B Target.
+
+ The RC-S956 can discover Type B Targets (Type 4B Tag) at 106
+ kbps. For a Type 4B Tag the firmware automatically sends an
+ ATTRIB command that configures the use of DID and 64 byte
+ maximum frame size. The driver reverts this configuration with
+ a DESELECT and WUPB command to return the target prepared for
+ activation (which nfcpy does in the tag activation code).
+
+ """
+ return super(Device, self).sense_ttb(target)
+
+ def sense_ttf(self, target):
+ """Activate the RF field and probe for a Type F Target. Bitrates 212
+ and 424 kpbs are supported.
+
+ """
+ return super(Device, self).sense_ttf(target)
+
+ def sense_dep(self, target):
+ """Search for a DEP Target. Both passive and passive communication
+ mode are supported.
+
+ """
+ return super(Device, self).sense_dep(target)
+
+ def listen_tta(self, target, timeout):
+ """Listen as Type A Target is not supported."""
+ info = "{device} does not support listen as Type A Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def listen_ttb(self, target, timeout):
+ """Listen as Type B Target is not supported."""
+ info = "{device} does not support listen as Type B Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def listen_ttf(self, target, timeout):
+ """Listen as Type F Target is not supported."""
+ info = "{device} does not support listen as Type F Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def listen_dep(self, target, timeout):
+ """Listen as DEP Target is not supported."""
+ info = "{device} does not support listen as DEP Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def turn_on_led_and_buzzer(self):
+ """Buzz and turn red."""
+ self.chipset.set_buzzer_and_led_to_active()
+
+ def turn_off_led_and_buzzer(self):
+ """Back to green."""
+ self.chipset.set_buzzer_and_led_to_default()
+
+
+class Chipset(pn532.Chipset):
+ # Maximum size of a host command frame to the contactless chip.
+ host_command_frame_max_size = 254
+
+ # Supported BrTy (baud rate / modulation type) values for the
+ # InListPassiveTarget command. Corresponds to 106 kbps Type A, 212
+ # kbps Type F, 424 kbps Type F, and 106 kbps Type B. The value for
+ # 106 kbps Innovision Jewel Tag (although supported by PN532) is
+ # removed because the RID command can not be send.
+ in_list_passive_target_brty_range = (0, 1, 2, 3)
+
+ def __init__(self, transport):
+ self.transport = transport
+
+ # read ACR122U firmware version string
+ reader_version = self.ccid_xfr_block(bytearray.fromhex("FF00480000"))
+ if not reader_version.startswith(b"ACR122U"):
+ log.error("failed to retrieve ACR122U version string")
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ if int(chr(reader_version[7])) < 2:
+ log.error("{0} not supported, need 2.x".format(reader_version[7:]))
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ log.debug("initialize " + reader_version.decode())
+
+ # set icc power on
+ log.debug("CCID ICC-POWER-ON")
+ frame = bytearray.fromhex("62000000000000000000")
+ transport.write(frame)
+ transport.read(100)
+
+ # disable autodetection
+ log.debug("Set PICC Operating Parameters")
+ self.ccid_xfr_block(bytearray.fromhex("FF00517F00"))
+
+ # switch red/green led off/on
+ log.debug("Configure Buzzer and LED")
+ self.set_buzzer_and_led_to_default()
+
+ super(Chipset, self).__init__(transport, logger=log)
+
+ def close(self):
+ self.ccid_xfr_block(bytearray.fromhex("FF00400C0400000000"))
+ self.transport.close()
+ self.transport = None
+
+ def set_buzzer_and_led_to_default(self):
+ """Turn off buzzer and set LED to default (green only). """
+ self.ccid_xfr_block(bytearray.fromhex("FF00400E0400000000"))
+
+ def set_buzzer_and_led_to_active(self, duration_in_ms=300):
+ """Turn on buzzer and set LED to red only. The timeout here must exceed
+ the total buzzer/flash duration defined in bytes 5-8. """
+ duration_in_tenths_of_second = int(min(duration_in_ms / 100, 255))
+ timeout_in_seconds = (duration_in_tenths_of_second + 1) / 10.0
+ data = "FF00400D04{:02X}000101".format(duration_in_tenths_of_second)
+ self.ccid_xfr_block(bytearray.fromhex(data),
+ timeout=timeout_in_seconds)
+
+ def send_ack(self):
+ # Send an ACK frame, usually to terminate most recent command.
+ self.ccid_xfr_block(Chipset.ACK)
+
+ def ccid_xfr_block(self, data, timeout=0.1):
+ """Encapsulate host command *data* into an PC/SC Escape command to
+ send to the device and extract the chip response if received
+ within *timeout* seconds.
+
+ """
+ frame = struct.pack("
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+#
+# Driver for the Arygon contactless reader with USB serial interface
+#
+from . import pn531
+from . import pn532
+
+import os
+import time
+import errno
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class ChipsetA(pn531.Chipset):
+ def write_frame(self, frame):
+ self.transport.write(b"2" + frame)
+
+
+class DeviceA(pn531.Device):
+ def close(self):
+ self.chipset.transport.tty.write(b"0au") # device reset
+ self.chipset.close()
+ self.chipset = None
+
+
+class ChipsetB(pn532.Chipset):
+ def write_frame(self, frame):
+ self.transport.write(b"2" + frame)
+
+
+class DeviceB(pn532.Device):
+ def close(self):
+ self.chipset.transport.tty.write(b"0au") # device reset
+ self.chipset.close()
+ self.chipset = None
+
+
+def init(transport):
+ transport.open(transport.port, 115200)
+ transport.tty.write(b"0av") # read version
+ response = transport.tty.readline()
+ if response.startswith(b"FF00000600V"):
+ log.debug("Arygon Reader AxxB Version %s",
+ response[11:].strip().decode())
+ transport.tty.timeout = 0.5
+ transport.tty.write(b"0at05")
+ if transport.tty.readline().startswith(b"FF0000"):
+ log.debug("MCU/TAMA communication set to 230400 bps")
+ transport.tty.write(b"0ah05")
+ if transport.tty.readline().startswith(b"FF0000"):
+ log.debug("MCU/HOST communication set to 230400 bps")
+ transport.tty.baudrate = 230400
+ transport.tty.timeout = 0.1
+ time.sleep(0.1)
+ chipset = ChipsetB(transport, logger=log)
+ device = DeviceB(chipset, logger=log)
+ device._vendor_name = "Arygon"
+ device._device_name = "ADRB"
+ return device
+
+ transport.open(transport.port, 9600)
+ transport.tty.write(b"0av") # read version
+ response = transport.tty.readline()
+ if response.startswith(b"FF00000600V"):
+ log.debug("Arygon Reader AxxA Version %s",
+ response[11:].strip().decode())
+ transport.tty.timeout = 0.5
+ transport.tty.write(b"0at05")
+ if transport.tty.readline().startswith(b"FF0000"):
+ log.debug("MCU/TAMA communication set to 230400 bps")
+ transport.tty.write(b"0ah05")
+ if transport.tty.readline().startswith(b"FF0000"):
+ log.debug("MCU/HOST communication set to 230400 bps")
+ transport.tty.baudrate = 230400
+ transport.tty.timeout = 0.1
+ time.sleep(0.1)
+ chipset = ChipsetA(transport, logger=log)
+ device = DeviceA(chipset, logger=log)
+ device._vendor_name = "Arygon"
+ device._device_name = "ADRA"
+ return device
+
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
diff --git a/src/lib/nfc/clf/device.py b/src/lib/nfc/clf/device.py
new file mode 100644
index 0000000..e0c6695
--- /dev/null
+++ b/src/lib/nfc/clf/device.py
@@ -0,0 +1,660 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""All contactless drivers must implement the interface defined in
+:class:`~nfc.clf.device.Device`. Unsupported target discovery or target
+emulation methods raise :exc:`~nfc.clf.UnsupportedTargetError`. The
+interface is used internally by :class:`~nfc.clf.ContactlessFrontend`
+and is not intended as an application programming interface. Device
+driver methods are not thread-safe and do not necessarily check input
+arguments when they are supposed to be valid. The interface may change
+without notice at any time.
+
+"""
+from . import transport
+
+import os
+import sys
+import errno
+import importlib
+
+import logging
+log = logging.getLogger(__name__)
+
+usb_device_map = {
+ (0x054c, 0x0193): "pn531", # PN531 (Sony VID/PID)
+ (0x04cc, 0x0531): "pn531", # PN531 (Philips VID/PID), SCM SCL3710
+ (0x04cc, 0x2533): "pn533", # NXP PN533 demo board
+ (0x04e6, 0x5591): "pn533", # SCM SCL3711
+ (0x04e6, 0x5593): "pn533", # SCM SCL3712
+ (0x054c, 0x02e1): "rcs956", # Sony RC-S330/360/370
+ (0x054c, 0x06c1): "rcs380", # Sony RC-S380
+ (0x054c, 0x06c3): "rcs380", # Sony RC-S380
+ (0x072f, 0x2200): "acr122", # ACS ACR122U
+}
+
+tty_driver_list = ["arygon", "pn532"]
+
+
+def connect(path):
+ """Connect to a local device identified by *path* and load the
+ appropriate device driver. The *path* argument is documented at
+ :meth:`nfc.clf.ContactlessFrontend.open`. The return value is
+ either a :class:`Device` instance or :const:`None`. Note that not
+ all drivers can be autodetected, specifically for serial devices
+ *path* must usually also specify the driver.
+
+ """
+ assert isinstance(path, str) and len(path) > 0
+
+ found = transport.USB.find(path)
+ if found is not None:
+ for vid, pid, bus, dev in found:
+ module = usb_device_map.get((vid, pid))
+ if module is None:
+ continue
+
+ log.debug("loading {mod} driver for usb:{vid:04x}:{pid:04x}"
+ .format(mod=module, vid=vid, pid=pid))
+
+ if sys.platform.startswith("linux"):
+ devnode = "/dev/bus/usb/%03d/%03d" % (int(bus), int(dev))
+ if not os.access(devnode, os.R_OK | os.W_OK):
+ log.debug("access denied to " + devnode)
+ if len(path.split(':')) < 3:
+ continue
+ else:
+ raise IOError(errno.EACCES, os.strerror(errno.EACCES))
+
+ driver = importlib.import_module("nfc.clf." + module)
+ try:
+ device = driver.init(transport.USB(bus, dev))
+ except IOError as error:
+ log.debug(error)
+ if len(path.split(':')) < 3:
+ continue
+ else:
+ raise error
+
+ device._path = "usb:{0:03}:{1:03}".format(int(bus), int(dev))
+ return device
+
+ found = transport.TTY.find(path)
+ if found is not None:
+ devices = found[0]
+ drivers = [found[1]] if found[1] else tty_driver_list
+ globbed = found[2] or drivers is tty_driver_list
+ for drv in drivers:
+ for dev in devices:
+ log.debug("trying {0} on {1}".format(drv, dev))
+ driver = importlib.import_module("src.lib.nfc.clf." + drv)
+ tty = None
+ try:
+ tty = transport.TTY(dev)
+ device = driver.init(tty)
+ device._path = dev
+ return device
+ except IOError as error:
+ log.debug(error)
+ if tty is not None:
+ tty.close()
+ if not globbed:
+ raise
+
+ if path.startswith("udp"):
+ path = path.split(':')
+ host = str(path[1]) if len(path) > 1 and path[1] else 'localhost'
+ port = int(path[2]) if len(path) > 2 and path[2] else 54321
+ driver = importlib.import_module("nfc.clf.udp")
+ device = driver.init(host, port)
+ device._path = "udp:{0}:{1}".format(host, port)
+ return device
+
+
+class Device(object):
+ """All device drivers inherit from the :class:`Device` class and must
+ implement it's methods.
+
+ """
+ def __init__(self, *args, **kwargs):
+ fname = "__init__"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def __str__(self):
+ strings = (self.vendor_name, self.product_name, self.chipset_name)
+ return ' '.join(filter(bool, strings)) + " at " + self.path
+
+ @property
+ def vendor_name(self):
+ """The device vendor name. An empty string if the vendor name could
+ not be determined.
+
+ """
+ return self._vendor_name if hasattr(self, "_vendor_name") else ''
+
+ @property
+ def product_name(self):
+ """The device product name. An empty string if the product name could
+ not be determined.
+
+ """
+ return self._device_name if hasattr(self, "_device_name") else ''
+
+ @property
+ def chipset_name(self):
+ """The name of the chipset embedded in the device."""
+ return self._chipset_name
+
+ @property
+ def path(self):
+ return self._path
+
+ def close(self):
+ fname = "close"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def mute(self):
+ """Mutes all existing communication, most notably the device will no
+ longer generate a 13.56 MHz carrier signal when operating as
+ Initiator.
+
+ """
+ fname = "mute"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def sense_tta(self, target):
+ """Discover a Type A Target.
+
+ Activates the 13.56 MHz carrier signal and sends a SENS_REQ
+ command at the bitrate set by **target.brty**. If a response
+ is received, sends an RID_CMD for a Type 1 Tag or SDD_REQ and
+ SEL_REQ for a Type 2/4 Tag and returns the responses.
+
+ Arguments:
+
+ target (nfc.clf.RemoteTarget): Supplies bitrate and optional
+ command data for the target discovery. The only sensible
+ command to set is **sel_req** populated with a UID to find
+ only that specific target.
+
+ Returns:
+
+ nfc.clf.RemoteTarget: Response data received from a remote
+ target if found. This includes at least **sens_res** and
+ either **rid_res** (for a Type 1 Tag) or **sdd_res** and
+ **sel_res** (for a Type 2/4 Tag).
+
+ Raises:
+
+ nfc.clf.UnsupportedTargetError: The method is not supported
+ or the *target* argument requested an unsupported bitrate
+ (or has a wrong technology type identifier).
+
+ """
+ fname = "sense_tta"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def sense_ttb(self, target):
+ """Discover a Type B Target.
+
+ Activates the 13.56 MHz carrier signal and sends a SENSB_REQ
+ command at the bitrate set by **target.brty**. If a SENSB_RES
+ is received, returns a target object with the **sensb_res**
+ attribute.
+
+ Note that the firmware of some devices (least all those based
+ on PN53x) automatically sends an ATTRIB command with varying
+ but always unfortunate communication settings. The drivers
+ correct that situation by sending S(DESELECT) and WUPB before
+ return.
+
+ Arguments:
+
+ target (nfc.clf.RemoteTarget): Supplies bitrate and the
+ optional **sensb_req** for target discovery. Most drivers
+ do no not allow a fully customized SENSB_REQ, the only
+ parameter that can always be changed is the AFI byte,
+ others may be ignored.
+
+ Returns:
+
+ nfc.clf.RemoteTarget: Response data received from a remote
+ target if found. The only response data attribute is
+ **sensb_res**.
+
+ Raises:
+
+ nfc.clf.UnsupportedTargetError: The method is not supported
+ or the *target* argument requested an unsupported bitrate
+ (or has a wrong technology type identifier).
+
+ """
+ fname = "sense_ttb"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def sense_ttf(self, target):
+ """Discover a Type F Target.
+
+ Activates the 13.56 MHz carrier signal and sends a SENSF_REQ
+ command at the bitrate set by **target.brty**. If a SENSF_RES
+ is received, returns a target object with the **sensf_res**
+ attribute.
+
+ Arguments:
+
+ target (nfc.clf.RemoteTarget): Supplies bitrate and the
+ optional **sensf_req** for target discovery. The default
+ SENSF_REQ invites all targets to respond and requests the
+ system code information bytes.
+
+ Returns:
+
+ nfc.clf.RemoteTarget: Response data received from a remote
+ target if found. The only response data attribute is
+ **sensf_res**.
+
+ Raises:
+
+ nfc.clf.UnsupportedTargetError: The method is not supported
+ or the *target* argument requested an unsupported bitrate
+ (or has a wrong technology type identifier).
+
+ """
+ fname = "sense_ttf"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def sense_dep(self, target):
+ """Discover a NFC-DEP Target in active communication mode.
+
+ Activates the 13.56 MHz carrier signal and sends an ATR_REQ
+ command at the bitrate set by **target.brty**. If an ATR_RES
+ is received, returns a target object with the **atr_res**
+ attribute.
+
+ Note that some drivers (like pn531) may modify the transport
+ data bytes length reduction value in ATR_REQ and ATR_RES due
+ to hardware limitations.
+
+ Arguments:
+
+ target (nfc.clf.RemoteTarget): Supplies bitrate and the
+ mandatory **atr_req** for target discovery. The bitrate
+ may be one of '106A', '212F', or '424F'.
+
+ Returns:
+
+ nfc.clf.RemoteTarget: Response data received from a remote
+ target if found. The only response data attribute is
+ **atr_res**. The actually sent and potentially modified
+ ATR_REQ is also included as **atr_req** attribute.
+
+ Raises:
+
+ nfc.clf.UnsupportedTargetError: The method is not supported
+ or the *target* argument requested an unsupported bitrate
+ (or has a wrong technology type identifier).
+
+ """
+ fname = "sense_dep"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def listen_tta(self, target, timeout):
+ """Listen as Type A Target.
+
+ Waits to receive a SENS_REQ command at the bitrate set by
+ **target.brty** and sends the **target.sens_res**
+ response. Depending on the SENS_RES bytes, the Initiator then
+ sends an RID_CMD (SENS_RES coded for a Type 1 Tag) or SDD_REQ
+ and SEL_REQ (SENS_RES coded for a Type 2/4 Tag). Responses are
+ then generated from the **rid_res** or **sdd_res** and
+ **sel_res** attributes in *target*.
+
+ Note that none of the currently supported hardware can
+ actually receive an RID_CMD, thus Type 1 Tag emulation is
+ impossible.
+
+ Arguments:
+
+ target (nfc.clf.LocalTarget): Supplies bitrate and mandatory
+ response data to reply when being discovered.
+
+ timeout (float): The maximum number of seconds to wait for a
+ discovery command.
+
+ Returns:
+
+ nfc.clf.LocalTarget: Command data received from the remote
+ Initiator if being discovered and to the extent supported
+ by the device. The first command received after discovery
+ is returned as one of the **tt1_cmd**, **tt2_cmd** or
+ **tt4_cmd** attribute (note that unset attributes are
+ always None).
+
+ Raises:
+
+ nfc.clf.UnsupportedTargetError: The method is not supported
+ or the *target* argument requested an unsupported bitrate
+ (or has a wrong technology type identifier).
+
+ ~exceptions.ValueError: A required target response attribute
+ is not present or does not supply the number of bytes
+ expected.
+
+ """
+ fname = "listen_tta"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def listen_ttb(self, target, timeout):
+ """Listen as Type A Target.
+
+ Waits to receive a SENSB_REQ command at the bitrate set by
+ **target.brty** and sends the **target.sensb_res**
+ response.
+
+ Note that none of the currently supported hardware can
+ actually listen as Type B target.
+
+ Arguments:
+
+ target (nfc.clf.LocalTarget): Supplies bitrate and mandatory
+ response data to reply when being discovered.
+
+ timeout (float): The maximum number of seconds to wait for a
+ discovery command.
+
+ Returns:
+
+ nfc.clf.LocalTarget: Command data received from the remote
+ Initiator if being discovered and to the extent supported
+ by the device. The first command received after discovery
+ is returned as **tt4_cmd** attribute.
+
+ Raises:
+
+ nfc.clf.UnsupportedTargetError: The method is not supported
+ or the *target* argument requested an unsupported bitrate
+ (or has a wrong technology type identifier).
+
+ ~exceptions.ValueError: A required target response attribute
+ is not present or does not supply the number of bytes
+ expected.
+
+ """
+ fname = "listen_ttb"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def listen_ttf(self, target, timeout):
+ """Listen as Type A Target.
+
+ Waits to receive a SENSF_REQ command at the bitrate set by
+ **target.brty** and sends the **target.sensf_res**
+ response. Then waits for a first command that is not a
+ SENSF_REQ and returns this as the **tt3_cmd** attribute.
+
+ Arguments:
+
+ target (nfc.clf.LocalTarget): Supplies bitrate and mandatory
+ response data to reply when being discovered.
+
+ timeout (float): The maximum number of seconds to wait for a
+ discovery command.
+
+ Returns:
+
+ nfc.clf.LocalTarget: Command data received from the remote
+ Initiator if being discovered and to the extent supported
+ by the device. The first command received after discovery
+ is returned as **tt3_cmd** attribute.
+
+ Raises:
+
+ nfc.clf.UnsupportedTargetError: The method is not supported
+ or the *target* argument requested an unsupported bitrate
+ (or has a wrong technology type identifier).
+
+ ~exceptions.ValueError: A required target response attribute
+ is not present or does not supply the number of bytes
+ expected.
+
+ """
+ fname = "listen_ttf"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def listen_dep(self, target, timeout):
+ """Listen as NFC-DEP Target.
+
+ Waits to receive an ATR_REQ (if the local device supports
+ active communication mode) or a Type A or F Target activation
+ followed by an ATR_REQ in passive communication mode. The
+ ATR_REQ is replied with **target.atr_res**. The first DEP_REQ
+ command is returned as the **dep_req** attribute along with
+ **atr_req** and **atr_res**. The **psl_req** and **psl_res**
+ attributes are returned when the has Initiator performed a
+ parameter selection. The **sens_res** or **sensf_res**
+ attributes are returned when activation was in passive
+ communication mode.
+
+ Arguments:
+
+ target (nfc.clf.LocalTarget): Supplies mandatory response
+ data to reply when being discovered. All of **sens_res**,
+ **sdd_res**, **sel_res**, **sensf_res**, and **atr_res**
+ must be provided. The bitrate does not need to be set, an
+ NFC-DEP Target always accepts discovery at '106A', '212F
+ and '424F'.
+
+ timeout (float): The maximum number of seconds to wait for a
+ discovery command.
+
+ Returns:
+
+ nfc.clf.LocalTarget: Command data received from the remote
+ Initiator if being discovered and to the extent supported
+ by the device. The first command received after discovery
+ is returned as **dep_req** attribute.
+
+ Raises:
+
+ nfc.clf.UnsupportedTargetError: The method is not supported
+ by the local hardware.
+
+ ~exceptions.ValueError: A required target response attribute
+ is not present or does not supply the number of bytes
+ expected.
+
+ """
+ fname = "listen_dep"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def send_cmd_recv_rsp(self, target, data, timeout):
+ """Exchange data with a remote Target
+
+ Sends command *data* to the remote *target* discovered in the
+ most recent call to one of the sense_xxx() methods. Note that
+ *target* becomes invalid with any call to mute(), sense_xxx()
+ or listen_xxx()
+
+ Arguments:
+
+ target (nfc.clf.RemoteTarget): The target returned by the
+ last successful call of a sense_xxx() method.
+
+ data (bytearray): The binary data to send to the remote
+ device.
+
+ timeout (float): The maximum number of seconds to wait for
+ response data from the remote device.
+
+ Returns:
+
+ bytearray: Response data received from the remote device.
+
+ Raises:
+
+ nfc.clf.CommunicationError: When no data was received.
+
+ """
+ fname = "send_cmd_recv_rsp"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def send_rsp_recv_cmd(self, target, data, timeout=None):
+ """Exchange data with a remote Initiator
+
+ Sends response *data* as the local *target* being discovered
+ in the most recent call to one of the listen_xxx() methods.
+ Note that *target* becomes invalid with any call to mute(),
+ sense_xxx() or listen_xxx()
+
+ Arguments:
+
+ target (nfc.clf.LocalTarget): The target returned by the
+ last successful call of a listen_xxx() method.
+
+ data (bytearray): The binary data to send to the remote
+ device.
+
+ timeout (float): The maximum number of seconds to wait for
+ command data from the remote device.
+
+ Returns:
+
+ bytearray: Command data received from the remote device.
+
+ Raises:
+
+ nfc.clf.CommunicationError: When no data was received.
+
+ """
+ fname = "send_rsp_recv_cmd"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def get_max_send_data_size(self, target):
+ """Returns the maximum number of data bytes for sending.
+
+ The maximum number of data bytes acceptable for sending with
+ either :meth:`send_cmd_recv_rsp` or :meth:`send_rsp_recv_cmd`.
+ The value reflects the local device capabilities for sending
+ in the mode determined by *target*. It does not relate to any
+ protocol capabilities and negotiations.
+
+ Arguments:
+
+ target (nfc.clf.Target): The current local or remote
+ communication target.
+
+ Returns:
+
+ int: Maximum number of data bytes supported for sending.
+
+ """
+ fname = "get_max_send_data_size"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def get_max_recv_data_size(self, target):
+ """Returns the maximum number of data bytes for receiving.
+
+ The maximum number of data bytes acceptable for receiving with
+ either :meth:`send_cmd_recv_rsp` or :meth:`send_rsp_recv_cmd`.
+ The value reflects the local device capabilities for receiving
+ in the mode determined by *target*. It does not relate to any
+ protocol capabilities and negotiations.
+
+ Arguments:
+
+ target (nfc.clf.Target): The current local or remote
+ communication target.
+
+ Returns:
+
+ int: Maximum number of data bytes supported for receiving.
+
+ """
+ fname = "get_max_recv_data_size"
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError("%s.%s() is required" % (cname, fname))
+
+ def turn_on_led_and_buzzer(self):
+ """If a device has an LED and/or a buzzer, this method can be
+ implemented to turn those indicators to the ON state.
+
+ """
+ pass
+
+ def turn_off_led_and_buzzer(self):
+ """If a device has an LED and/or a buzzer, this method can be
+ implemented to turn those indicators to the OFF state.
+
+ """
+ pass
+
+ @staticmethod
+ def add_crc_a(data):
+ # Calculate CRC-A for bytearray *data* and return *data*
+ # extended with the two CRC bytes.
+ crc = calculate_crc(data, len(data), 0x6363)
+ return data + bytearray([crc & 0xff, crc >> 8])
+
+ @staticmethod
+ def check_crc_a(data):
+ # Calculate CRC-A for the leading *len(data)-2* bytes of
+ # bytearray *data* and return whether the result matches the
+ # trailing 2 bytes of *data*.
+ crc = calculate_crc(data, len(data)-2, 0x6363)
+ return (data[-2], data[-1]) == (crc & 0xff, crc >> 8)
+
+ @staticmethod
+ def add_crc_b(data):
+ # Calculate CRC-B for bytearray *data* and return *data*
+ # extended with the two CRC bytes.
+ crc = ~calculate_crc(data, len(data), 0xFFFF) & 0xFFFF
+ return data + bytearray([crc & 0xff, crc >> 8])
+
+ @staticmethod
+ def check_crc_b(data):
+ # Calculate CRC-B for the leading *len(data)-2* bytes of
+ # bytearray *data* and return whether the result matches the
+ # trailing 2 bytes of *data*.
+ crc = ~calculate_crc(data, len(data)-2, 0xFFFF) & 0xFFFF
+ return (data[-2], data[-1]) == (crc & 0xff, crc >> 8)
+
+
+def calculate_crc(data, size, reg):
+ for octet in data[:size]:
+ for pos in range(8):
+ bit = (reg ^ ((octet >> pos) & 1)) & 1
+ reg = reg >> 1
+ if bit:
+ reg = reg ^ 0x8408
+ return reg
diff --git a/src/lib/nfc/clf/pn531.py b/src/lib/nfc/clf/pn531.py
new file mode 100644
index 0000000..8153922
--- /dev/null
+++ b/src/lib/nfc/clf/pn531.py
@@ -0,0 +1,316 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""Driver module for contactless devices based on the NXP PN531
+chipset. This was once a (sort of) joint development between Philips
+and Sony to supply hardware capable of running the ISO/IEC 18092 Data
+Exchange Protocol. The chip has selectable UART, I2C, SPI, or USB host
+interfaces, For USB the vendor and product ID can be switched by a
+hardware pin to either Philips or Sony.
+
+The internal chipset architecture comprises a small 8-bit MCU and a
+Contactless Interface Unit CIU that is basically a PN511. The CIU
+implements the analog and digital part of communication (modulation
+and framing) while the MCU handles the protocol parts and host
+communication. The PN511 and hence the PN531 does not support Type B
+Technology and can not handle the specific Jewel/Topaz (Type 1 Tag)
+communication. Compared to PN532/PN533 the host frame structure does
+not allow maximum size ISO/IEC 18092 packets to be transferred. The
+driver handles this restriction by modifying the initialization
+commands (ATR, PSL) when needed.
+
+========== ======= ============
+function support remarks
+========== ======= ============
+sense_tta yes Type 1 Tag is not supported
+sense_ttb no
+sense_ttf yes
+sense_dep yes Reduced transport data byte length (max 192)
+listen_tta yes
+listen_ttb no
+listen_ttf yes Maximimum frame size is 64 byte
+listen_dep yes
+========== ======= ============
+
+"""
+import nfc.clf
+from . import pn53x
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Chipset(pn53x.Chipset):
+ CMD = {
+ # Miscellaneous
+ 0x00: "Diagnose",
+ 0x02: "GetFirmwareVersion",
+ 0x04: "GetGeneralStatus",
+ 0x06: "ReadRegister",
+ 0x08: "WriteRegister",
+ 0x0C: "ReadGPIO",
+ 0x0E: "WriteGPIO",
+ 0x10: "SetSerialBaudrate",
+ 0x12: "SetTAMAParameters",
+ 0x14: "SAMConfiguration",
+ 0x16: "PowerDown",
+ # RF communication
+ 0x32: "RFConfiguration",
+ 0x58: "RFRegulationTest",
+ # Initiator
+ 0x56: "InJumpForDEP",
+ 0x46: "InJumpForPSL",
+ 0x4A: "InListPassiveTarget",
+ 0x50: "InATR",
+ 0x4E: "InPSL",
+ 0x40: "InDataExchange",
+ 0x42: "InCommunicateThru",
+ 0x44: "InDeselect",
+ 0x52: "InRelease",
+ 0x54: "InSelect",
+ # Target
+ 0x8C: "TgInitTAMATarget",
+ 0x92: "TgSetGeneralBytes",
+ 0x86: "TgGetDEPData",
+ 0x8E: "TgSetDEPData",
+ 0x94: "TgSetMetaDEPData",
+ 0x88: "TgGetInitiatorCommand",
+ 0x90: "TgResponseToInitiator",
+ 0x8A: "TgGetTargetStatus",
+ }
+ ERR = {
+ 0x01: "Time out, the Target has not answered",
+ 0x02: "Checksum error during RF communication",
+ 0x03: "Parity error during RF communication",
+ 0x04: "Erroneous bit count in anticollision",
+ 0x05: "Framing error during Mifare operation",
+ 0x06: "Abnormal bit collision in 106 kbps anticollision",
+ 0x07: "Insufficient communication buffer size",
+ 0x09: "RF buffer overflow detected by CIU",
+ 0x0a: "RF field not activated in time by active mode peer",
+ 0x0b: "Protocol error during RF communication",
+ 0x0d: "Overheated - antenna drivers deactivated",
+ 0x0e: "Internal buffer overflow",
+ 0x10: "Invalid command parameter",
+ 0x12: "Unsupported command from Initiator",
+ 0x13: "Format error during RF communication",
+ 0x14: "Mifare authentication error",
+ 0x23: "ISO/IEC14443-3 UID check byte is wrong",
+ 0x25: "Command invalid in current DEP state",
+ 0x26: "Operation not allowed in this configuration",
+ 0x27: "Command is not acceptable in the current context",
+ 0x7f: "Invalid command syntax - received error frame",
+ 0xff: "Insufficient data received from executing chip command",
+ }
+
+ host_command_frame_max_size = 254
+ """Maximum host command frame size."""
+
+ in_list_passive_target_max_target = 2
+ """Maximum number of targets for the InListPassiveTarget command."""
+
+ in_list_passive_target_brty_range = (0, 1, 2)
+ """Possible values for the brty parameter to InListPassiveTarget."""
+
+ def _read_register(self, data):
+ return self.command(0x06, data, timeout=0.25)
+
+ def _write_register(self, data):
+ self.command(0x08, data, timeout=0.25)
+
+ sam_configuration_modes = ("normal", "virtual", "wired", "dual")
+ """Possible SAM configuration modes."""
+
+ def sam_configuration(self, mode, timeout=0):
+ """Send the SAMConfiguration command to configure the Security Access
+ Module. The *mode* argument must be one of the string values
+ in :data:`sam_configuration_modes`. The *timeout* argument is
+ only relevant for the virtual card configuration mode.
+
+ """
+ mode = self.sam_configuration_modes.index(mode) + 1
+ self.command(0x14, bytearray([mode, timeout]), timeout=0.1)
+
+ power_down_wakeup_sources = ("INT0", "INT1", "USB", "RF", "HSU", "SPI")
+ """Possible wake up sources for the :meth:`power_down` method."""
+
+ def power_down(self, wakeup_enable):
+ """Send the PowerDown command to put the PN531 (including the
+ contactless analog front end) into power down mode in order to
+ save power consumption. The *wakeup_enable* argument must be a
+ list of wake up sources with values from the
+ :data:`power_down_wakeup_sources`.
+
+ """
+ wakeup_set = 0
+ for i, src in enumerate(self.power_down_wakeup_sources):
+ if src in wakeup_enable:
+ wakeup_set |= 1 << i
+ data = self.command(0x16, bytearray([wakeup_set]), timeout=0.1)
+ if data[0] != 0:
+ self.chipset_error(data)
+
+ def tg_init_tama_target(self, mode, mifare_params, felica_params,
+ nfcid3t, gt, timeout):
+ """Send the TgInitTAMATarget command."""
+ assert type(mode) is int and mode & 0b11111100 == 0
+ assert len(mifare_params) == 6
+ assert len(felica_params) == 18
+ assert len(nfcid3t) == 10
+
+ data = bytearray([mode]) + mifare_params + felica_params + nfcid3t + gt
+ return self.command(0x8c, data, timeout)
+
+
+class Device(pn53x.Device):
+ # Device driver for PN531 based contactless frontends.
+
+ def __init__(self, chipset, logger):
+ assert isinstance(chipset, Chipset)
+ super(Device, self).__init__(chipset, logger)
+
+ ver, rev = self.chipset.get_firmware_version()
+ self._chipset_name = "PN531v{0}.{1}".format(ver, rev)
+ self.log.debug("chipset is a {0}".format(self._chipset_name))
+
+ self.chipset.sam_configuration("normal")
+ self.chipset.set_parameters(0b00000000)
+ self.chipset.rf_configuration(0x02, b"\x00\x0B\x0A")
+ self.chipset.rf_configuration(0x04, b"\x00")
+ self.chipset.rf_configuration(0x05, b"\x01\x00\x01")
+ self.mute()
+
+ def close(self):
+ self.mute()
+ super(Device, self).close()
+
+ def sense_tta(self, target):
+ """Activate the RF field and probe for a Type A Target.
+
+ The PN531 can discover some Type A Targets (Type 2 Tag and
+ Type 4A Tag) at 106 kbps. Type 1 Tags (Jewel/Topaz) are
+ completely unsupported. Because the firmware does not evaluate
+ the SENS_RES before sending SDD_REQ, it may be that a warning
+ message about missing Type 1 Tag support is logged even if a
+ Type 2 or 4A Tag was present. This typically happens when the
+ SDD_RES or SEL_RES are lost due to communication errors
+ (normally when the tag is moved away).
+
+ """
+ target = super(Device, self).sense_tta(target)
+ if target and target.sdd_res and len(target.sdd_res) > 4:
+ # Remove the cascade tag(s) from SDD_RES, only the PN531
+ # has them included and we've set the policy that cascade
+ # tags are not part of the sel_req/sdd_res parameters.
+ if len(target.sdd_res) == 8:
+ target.sdd_res = target.sdd_res[1:]
+ elif len(target.sdd_res) == 12:
+ target.sdd_res = target.sdd_res[1:4] + target.sdd_res[5:]
+ # Also the SENS_RES bytes are reversed compared to PN532/533
+ target.sens_res = bytearray(reversed(target.sens_res))
+ return target
+
+ def sense_ttb(self, target):
+ """Sense for a Type B Target is not supported."""
+ info = "{device} does not support sense for Type B Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def sense_ttf(self, target):
+ """Activate the RF field and probe for a Type F Target.
+
+ """
+ return super(Device, self).sense_ttf(target)
+
+ def sense_dep(self, target):
+ """Search for a DEP Target in active communication mode.
+
+ Because the PN531 does not implement the extended frame syntax
+ for host controller communication, it can not support the
+ maximum payload size of 254 byte. The driver handles this by
+ modifying the length-reduction values in atr_req and atr_res.
+
+ """
+ if target.atr_req[15] & 0x30 == 0x30:
+ self.log.warning("must reduce the max payload size in atr_req")
+ target.atr_req[15] = (target.atr_req[15] & 0xCF) | 0x20
+
+ target = super(Device, self).sense_dep(target)
+ if target is None:
+ return
+
+ if target.atr_res[16] & 0x30 == 0x30:
+ self.log.warning("must reduce the max payload size in atr_res")
+ atr_res = bytearray(target.atr_res)
+ atr_res[16] = (target.atr_res[16] & 0xCF) | 0x20
+ target.atr_res = bytes(atr_res)
+
+ return target
+
+ def listen_tta(self, target, timeout):
+ """Listen *timeout* seconds for a Type A activation at 106 kbps. The
+ ``sens_res``, ``sdd_res``, and ``sel_res`` response data must
+ be provided and ``sdd_res`` must be a 4 byte UID that starts
+ with ``08h``. Depending on ``sel_res`` an activation may
+ return a target with a ``tt2_cmd``, ``tt4_cmd`` or ``atr_req``
+ attribute. The default RATS response sent for a Type 4 Tag
+ activation can be replaced with a ``rats_res`` attribute.
+
+ """
+ return super(Device, self).listen_tta(target, timeout)
+
+ def listen_ttb(self, target, timeout):
+ """Listen as Type B Target is not supported."""
+ info = "{device} does not support listen as Type B Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def listen_ttf(self, target, timeout):
+ """Listen *timeout* seconds for a Type F card activation. The target
+ ``brty`` must be set to either 212F or 424F and ``sensf_res``
+ provide 19 byte response data (response code + 8 byte IDm + 8
+ byte PMm + 2 byte system code). Note that the maximum command
+ an response frame length is 64 bytes only (including the frame
+ length byte), because the driver must directly program the
+ contactless interface unit within the PN533.
+
+ """
+ return super(Device, self).listen_ttf(target, timeout)
+
+ def listen_dep(self, target, timeout):
+ """Listen *timeout* seconds to become initialized as a DEP Target.
+
+ The PN531 can be set to listen as a DEP Target for passive and
+ active communication mode.
+
+ """
+ return super(Device, self).listen_dep(target, timeout)
+
+ def _init_as_target(self, mode, tta_params, ttf_params, timeout):
+ nfcid3t = ttf_params[0:8] + b"\x00\x00"
+ args = (mode, tta_params, ttf_params, nfcid3t, b'', timeout)
+ return self.chipset.tg_init_tama_target(*args)
+
+
+def init(transport):
+ chipset = Chipset(transport, logger=log)
+ device = Device(chipset, logger=log)
+ device._vendor_name = transport.manufacturer_name
+ device._device_name = transport.product_name
+ return device
diff --git a/src/lib/nfc/clf/pn532.py b/src/lib/nfc/clf/pn532.py
new file mode 100644
index 0000000..d6469e1
--- /dev/null
+++ b/src/lib/nfc/clf/pn532.py
@@ -0,0 +1,454 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""Driver module for contactless devices based on the NXP PN532
+chipset. This successor of the PN531 can additionally handle Type B
+Technology (type 4B Tags) and Type 1 Tag communication. It also
+supports an extended frame syntax for host communication that allows
+larger packets to be transferred. The chip has selectable UART, I2C or
+SPI host interfaces. A speciality of the PN532 is that it can manage
+two targets (cards) simultanously, although this is not used by
+*nfcpy*.
+
+The internal chipset architecture comprises a small 8-bit MCU and a
+Contactless Interface Unit CIU that is basically a PN512. The CIU
+implements the analog and digital part of communication (modulation
+and framing) while the MCU handles the protocol parts and host
+communication. Almost all PN532 firmware limitations (or bugs) can be
+avoided by directly programming the CIU. Type F Target mode for card
+emulation is completely implemented with the CIU and limited to 64
+byte frame exchanges by the CIU's FIFO size. Type B Target mode is not
+possible.
+
+========== ======= ============
+function support remarks
+========== ======= ============
+sense_tta yes
+sense_ttb yes
+sense_ttf yes
+sense_dep yes
+listen_tta yes
+listen_ttb no
+listen_ttf yes Maximimum frame size is 64 byte
+listen_dep yes
+========== ======= ============
+
+"""
+import src.lib.nfc.clf
+from . import pn53x
+
+import os
+import sys
+import time
+import errno
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Chipset(pn53x.Chipset):
+ CMD = {
+ # Miscellaneous
+ 0x00: "Diagnose",
+ 0x02: "GetFirmwareVersion",
+ 0x04: "GetGeneralStatus",
+ 0x06: "ReadRegister",
+ 0x08: "WriteRegister",
+ 0x0C: "ReadGPIO",
+ 0x0E: "WriteGPIO",
+ 0x10: "SetSerialBaudrate",
+ 0x12: "SetParameters",
+ 0x14: "SAMConfiguration",
+ 0x16: "PowerDown",
+ # RF communication
+ 0x32: "RFConfiguration",
+ 0x58: "RFRegulationTest",
+ # Initiator
+ 0x56: "InJumpForDEP",
+ 0x46: "InJumpForPSL",
+ 0x4A: "InListPassiveTarget",
+ 0x50: "InATR",
+ 0x4E: "InPSL",
+ 0x40: "InDataExchange",
+ 0x42: "InCommunicateThru",
+ 0x44: "InDeselect",
+ 0x52: "InRelease",
+ 0x54: "InSelect",
+ 0x60: "InAutoPoll",
+ # Target
+ 0x8C: "TgInitAsTarget",
+ 0x92: "TgSetGeneralBytes",
+ 0x86: "TgGetData",
+ 0x8E: "TgSetData",
+ 0x94: "TgSetMetaData",
+ 0x88: "TgGetInitiatorCommand",
+ 0x90: "TgResponseToInitiator",
+ 0x8A: "TgGetTargetStatus",
+ }
+ ERR = {
+ 0x01: "Time out, the Target has not answered",
+ 0x02: "Checksum error during RF communication",
+ 0x03: "Parity error during RF communication",
+ 0x04: "Erroneous bit count in anticollision",
+ 0x05: "Framing error during Mifare operation",
+ 0x06: "Abnormal bit collision in 106 kbps anticollision",
+ 0x07: "Insufficient communication buffer size",
+ 0x09: "RF buffer overflow detected by CIU",
+ 0x0a: "RF field not activated in time by active mode peer",
+ 0x0b: "Protocol error during RF communication",
+ 0x0d: "Overheated - antenna drivers deactivated",
+ 0x0e: "Internal buffer overflow",
+ 0x10: "Invalid command parameter",
+ 0x12: "Unsupported command from Initiator",
+ 0x13: "Format error during RF communication",
+ 0x14: "Mifare authentication error",
+ 0x23: "ISO/IEC14443-3 UID check byte is wrong",
+ 0x25: "Command invalid in current DEP state",
+ 0x26: "Operation not allowed in this configuration",
+ 0x27: "Command is not acceptable in the current context",
+ 0x29: "Released by Initiator while operating as Target",
+ 0x2A: "ISO/IEC14443-3B, the ID of the card does not match",
+ 0x2B: "ISO/IEC14443-3B, card previously activated has disappeared",
+ 0x2C: "NFCID3i and NFCID3t mismatch in DEP 212/424 kbps passive",
+ 0x2D: "An over-current event has been detected",
+ 0x2E: "NAD missing in DEP frame",
+ 0x7f: "Invalid command syntax - received error frame",
+ 0xff: "Insufficient data received from executing chip command",
+ }
+
+ host_command_frame_max_size = 265
+ in_list_passive_target_max_target = 2
+ in_list_passive_target_brty_range = (0, 1, 2, 3, 4)
+
+ def _read_register(self, data):
+ return self.command(0x06, data, timeout=0.25)
+
+ def _write_register(self, data):
+ self.command(0x08, data, timeout=0.25)
+
+ def set_serial_baudrate(self, baudrate):
+ br = (9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1288000)
+
+ self.command(0x10, bytearray([br.index(baudrate)]), timeout=0.1)
+ self.write_frame(self.ACK)
+ time.sleep(0.001)
+
+ def sam_configuration(self, mode, timeout=0, irq=False):
+ mode = ("normal", "virtual", "wired", "dual").index(mode) + 1
+ self.command(0x14, bytearray([mode, timeout, int(irq)]), timeout=0.1)
+
+ power_down_wakeup_src = ("INT0", "INT1", "rfu", "RF",
+ "HSU", "SPI", "GPIO", "I2C")
+
+ def power_down(self, wakeup_enable, generate_irq=False):
+ wakeup_set = 0
+ for i, src in enumerate(self.power_down_wakeup_src):
+ if src in wakeup_enable:
+ wakeup_set |= 1 << i
+ cmd_data = bytearray([wakeup_set, int(generate_irq)])
+ data = self.command(0x16, cmd_data, timeout=0.1)
+ if data[0] != 0:
+ self.chipset_error(data)
+
+ def tg_init_as_target(self, mode, mifare_params, felica_params, nfcid3t,
+ general_bytes=b'', historical_bytes=b'',
+ timeout=None):
+ assert type(mode) is int and mode & 0b11111000 == 0
+ assert len(mifare_params) == 6
+ assert len(felica_params) == 18
+ assert len(nfcid3t) == 10
+
+ data = (bytearray([mode]) + mifare_params + felica_params + nfcid3t +
+ bytearray([len(general_bytes)]) + general_bytes +
+ bytearray([len(historical_bytes)]) + historical_bytes)
+ return self.command(0x8c, data, timeout)
+
+
+class Device(pn53x.Device):
+ # Device driver for PN532 based contactless frontends.
+
+ def __init__(self, chipset, logger):
+ assert isinstance(chipset, Chipset)
+ super(Device, self).__init__(chipset, logger)
+
+ ic, ver, rev, support = self.chipset.get_firmware_version()
+ self._chipset_name = "PN5{0:02x}v{1}.{2}".format(ic, ver, rev)
+ self.log.debug("chipset is a {0}".format(self._chipset_name))
+
+ self.chipset.set_parameters(0b00000000)
+ self.chipset.rf_configuration(0x02, b"\x00\x0B\x0A")
+ self.chipset.rf_configuration(0x04, b"\x00")
+ self.chipset.rf_configuration(0x05, b"\x01\x00\x01")
+
+ self.log.debug("write analog settings for Type A 106 kbps")
+ data = bytearray.fromhex("59 F4 3F 11 4D 85 61 6F 26 62 87")
+ self.chipset.rf_configuration(0x0A, data)
+
+ self.log.debug("write analog settings for Type F 212/424 kbps")
+ data = bytearray.fromhex("69 FF 3F 11 41 85 61 6F")
+ self.chipset.rf_configuration(0x0B, data)
+
+ self.log.debug("write analog settings for Type B 106 kbps")
+ data = bytearray.fromhex("FF 04 85")
+ self.chipset.rf_configuration(0x0C, data)
+
+ self.log.debug("write analog settings for 14443-4 212/424/848 kbps")
+ data = bytearray.fromhex("85 15 8A 85 08 B2 85 01 DA")
+ self.chipset.rf_configuration(0x0D, data)
+
+ self.mute()
+
+ def close(self):
+ # Cancel most recent command in case we've been interrupted
+ # before the response, give the chip 10 ms to think about it.
+ self.chipset.send_ack()
+ time.sleep(0.01)
+
+ # When using the high speed uart we must set the baud rate
+ # back to 115.2 kbps, otherwise we can't talk next time.
+ if self.chipset.transport.TYPE == "TTY":
+ self.chipset.set_serial_baudrate(115200)
+ self.chipset.transport.baudrate = 115200
+
+ # Set the chip to sleep mode with some wakeup sources.
+ self.chipset.power_down(wakeup_enable=("I2C", "SPI", "HSU"))
+ super(Device, self).close()
+
+ def sense_tta(self, target):
+ """Search for a Type A Target.
+
+ The PN532 can discover all kinds of Type A Targets (Type 1
+ Tag, Type 2 Tag, and Type 4A Tag) at 106 kbps.
+
+ """
+ return super(Device, self).sense_tta(target)
+
+ def sense_ttb(self, target):
+ """Search for a Type B Target.
+
+ The PN532 can discover Type B Targets (Type 4B Tag) at 106
+ kbps. For a Type 4B Tag the firmware automatically sends an
+ ATTRIB command that configures the use of DID and 64 byte
+ maximum frame size. The driver reverts this configuration with
+ a DESELECT and WUPB command to return the target prepared for
+ activation (which nfcpy does in the tag activation code).
+
+ """
+ return super(Device, self).sense_ttb(target, did=b'\x01')
+
+ def sense_ttf(self, target):
+ """Search for a Type F Target.
+
+ The PN532 can discover Type F Targets (Type 3 Tag) at 212 and
+ 424 kbps. The driver uses the default polling command
+ ``06FFFF0000`` if no ``target.sens_req`` is supplied.
+
+ """
+ return super(Device, self).sense_ttf(target)
+
+ def sense_dep(self, target):
+ """Search for a DEP Target in active communication mode."""
+ return super(Device, self).sense_dep(target)
+
+ def _tt1_send_cmd_recv_rsp(self, data, timeout):
+ # Special handling for Tag Type 1 (Jewel/Topaz) card commands.
+
+ if data[0] in (0x00, 0x01, 0x1A, 0x53, 0x72):
+ # These commands are implemented by the chipset.
+ return self.chipset.in_data_exchange(data, timeout)[0]
+
+ if data[0] == 0x10:
+ # RSEG implementation does not accept any segment other
+ # than 0. Unfortunately we can not directly issue this
+ # command to the CIU because the response is 128 byte and
+ # we're not fast enough to read it from the 64 byte FIFO.
+ rsp = data[1:2]
+ for block in range((data[1] >> 4) * 16, (data[1] >> 4) * 16 + 16):
+ cmd = bytearray([0x02, block]) + data[2:]
+ rsp += self._tt1_send_cmd_recv_rsp(cmd, timeout)[1:9]
+ return rsp
+
+ # Remaining commands READ8, WRITE-E8, WRITE-NE8 are not
+ # implemented by the chipset. Fortunately we can directly
+ # program the CIU through register read/write. Each TT1
+ # command byte must be send as a separate Type A frame, the
+ # first as a short frame with only 7 data bits and the others
+ # as normal frames. Reading is also a bit complicated because
+ # for sending we have to disable the parity generator which
+ # means that we will also receive the parity bits, thus 9 bits
+ # received per 8 data bits. And because they are already
+ # reversed in the FIFO we must swap before parity removal and
+ # afterwards (maybe this could be optimized a bit)
+ data = self.add_crc_b(data)
+ register_write = []
+ register_write.append(("CIU_FIFOData", data[0])) # CMD_CODE
+ register_write.append(("CIU_BitFraming", 0x07)) # 7 bits
+ register_write.append(("CIU_Command", 0x04)) # Transmit
+ register_write.append(("CIU_BitFraming", 0x00)) # 8 bits
+ register_write.append(("CIU_ManualRCV", 0x30)) # ParityDisable
+ for i in range(1, len(data)):
+ register_write.append(("CIU_FIFOData", data[i])) # CMD_DATA
+ register_write.append(("CIU_Command", 0x04)) # Transmit
+ register_write.append(("CIU_Command", 0x07)) # NoCmdChange
+ register_write.append(("CIU_Command", 0x08)) # Receive
+ self.chipset.write_register(*register_write)
+ if data[0] == 0x54: # WRITE-E8
+ time.sleep(0.006) # assuming same response time as WRITE-E
+ if data[0] == 0x1B: # WRITE-NE8
+ time.sleep(0.003) # assuming same response time as WRITE-NE
+ self.chipset.write_register(("CIU_ManualRCV", 0x20)) # enable parity
+ fifo_level = self.chipset.read_register("CIU_FIFOLevel")
+ if fifo_level == 0:
+ raise nfc.clf.TimeoutError
+ data = self.chipset.read_register(*(fifo_level * ["CIU_FIFOData"]))
+ data = ''.join(["{:08b}".format(octet)[::-1] for octet in data])
+ data = [int(data[i:i+8][::-1], 2) for i in range(0, len(data)-8, 9)]
+ if self.check_crc_b(data) is False:
+ raise nfc.clf.TransmissionError("crc_b check error")
+ return bytearray(data[0:-2])
+
+ def listen_tta(self, target, timeout):
+ """Listen *timeout* seconds for a Type A activation at 106 kbps. The
+ ``sens_res``, ``sdd_res``, and ``sel_res`` response data must
+ be provided and ``sdd_res`` must be a 4 byte UID that starts
+ with ``08h``. Depending on ``sel_res`` an activation may
+ return a target with a ``tt2_cmd``, ``tt4_cmd`` or ``atr_req``
+ attribute. The default RATS response sent for a Type 4 Tag
+ activation can be replaced with a ``rats_res`` attribute.
+
+ """
+ return super(Device, self).listen_tta(target, timeout)
+
+ def listen_ttb(self, target, timeout):
+ """Listen as Type B Target is not supported."""
+ info = "{device} does not support listen as Type B Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def listen_ttf(self, target, timeout):
+ """Listen *timeout* seconds for a Type F card activation. The target
+ ``brty`` must be set to either 212F or 424F and ``sensf_res``
+ provide 19 byte response data (response code + 8 byte IDm + 8
+ byte PMm + 2 byte system code). Note that the maximum command
+ an response frame length is 64 bytes only (including the frame
+ length byte), because the driver must directly program the
+ contactless interface unit within the PN533.
+
+ """
+ return super(Device, self).listen_ttf(target, timeout)
+
+ def listen_dep(self, target, timeout):
+ """Listen *timeout* seconds to become initialized as a DEP Target.
+
+ The PN532 can be set to listen as a DEP Target for passive and
+ active communication mode.
+
+ """
+ return super(Device, self).listen_dep(target, timeout)
+
+ def _init_as_target(self, mode, tta_params, ttf_params, timeout):
+ nfcid3t = ttf_params[0:8] + b"\x00\x00"
+ args = (mode, tta_params, ttf_params, nfcid3t, b'', b'', timeout)
+ return self.chipset.tg_init_as_target(*args)
+
+
+def init(transport):
+ if transport.TYPE == "TTY":
+ baudrate = 115200 # PN532 initial baudrate
+ transport.open(transport.port, baudrate)
+ long_preamble = bytearray(10)
+
+ # The PN532 chip should send an ack within 15 ms after a
+ # command. We'll give it a bit more and wait 100 ms, unless
+ # we're on a Raspberry Pi detected by the Broadcom SOC. The
+ # USB on BCM270x has a nasty bug (may be SW or HW) that
+ # introduces additional up to ~1000 ms delay for the first
+ # data from a ttyUSB. Tested with two serial converters
+ # (PL2303 and FT232R) in loopback and it's reproducable adding
+ # up to 1000 ms if a serial open is done 1 sec after serial
+ # close. Waiting longer decreases that time until after 2 sec
+ # wait between close and open it all goes fine until the wait
+ # time reaches 3 seconds, and so on.
+ initial_timeout = 100 # milliseconds
+ # change_baudrate = True # try higher speeds
+ change_baudrate = False # MOD GG *DO NOT* try higher speeds
+ if sys.platform.startswith('linux'):
+ board = b"" # Raspi board will identify through device tree
+ try:
+ board = open('/proc/device-tree/model', "rb").read().strip(
+ b'\x00')
+ except IOError:
+ pass
+ if board.startswith(b"Raspberry Pi"):
+ log.debug("running on {}".format(board))
+ if transport.port.startswith("/dev/ttyUSB"):
+ log.debug("ttyUSB requires more time for first ack")
+ initial_timeout = 1500 # milliseconds
+ elif transport.port == "/dev/ttyS0":
+ log.debug("ttyS0 can only do 115.2 kbps")
+ change_baudrate = False # RPi 'mini uart'
+
+ get_version_cmd = bytearray.fromhex("0000ff02fed4022a00")
+ get_version_rsp = bytearray.fromhex("0000ff06fad50332")
+ transport.write(long_preamble + get_version_cmd)
+ log.debug("wait %d ms for data on %s", initial_timeout, transport.port)
+ if not transport.read(timeout=initial_timeout) == Chipset.ACK:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+ if not transport.read(timeout=100).startswith(get_version_rsp):
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ sam_configuration_cmd = bytearray.fromhex("0000ff05fbd4140100001700")
+ sam_configuration_rsp = bytearray.fromhex("0000ff02fed5151600")
+ transport.write(long_preamble + sam_configuration_cmd)
+ if not transport.read(timeout=100) == Chipset.ACK:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+ if not transport.read(timeout=100) == sam_configuration_rsp:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ if sys.platform.startswith("linux") and change_baudrate is True:
+ stty = 'stty -F %s %%d 2> /dev/null' % transport.port
+ # MOD GG FIXED BAUD RATE
+ # for baudrate in (921600, 460800, 230400, 115200):
+ for baudrate in (115200,):
+ log.debug("trying to set %d baud", baudrate)
+ if os.system(stty % baudrate) == 0:
+ os.system(stty % 115200)
+ break
+
+ if baudrate > 115200:
+ set_baudrate_cmd = bytearray.fromhex("0000ff03fdd410000000")
+ set_baudrate_rsp = bytearray.fromhex("0000ff02fed5111a00")
+ set_baudrate_cmd[7] = 5 + (230400, 460800, 921600).index(baudrate)
+ set_baudrate_cmd[8] = 256 - sum(set_baudrate_cmd[5:8])
+ transport.write(long_preamble + set_baudrate_cmd)
+ if not transport.read(timeout=100) == Chipset.ACK:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+ if not transport.read(timeout=100) == set_baudrate_rsp:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ transport.write(Chipset.ACK)
+ transport.open(transport.port, baudrate)
+ log.debug("changed uart speed to %d baud", baudrate)
+ time.sleep(0.001)
+
+ chipset = Chipset(transport, logger=log)
+ return Device(chipset, logger=log)
+
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
diff --git a/src/lib/nfc/clf/pn533.py b/src/lib/nfc/clf/pn533.py
new file mode 100644
index 0000000..d17fc4f
--- /dev/null
+++ b/src/lib/nfc/clf/pn533.py
@@ -0,0 +1,399 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""Driver module for contactless devices based on the NXP PN533
+chipset. The PN533 is pretty similar to the PN532 except that it also
+has a USB host interface option and, probably due to the resources
+needed for USB, does not support two simultaneous targets. Anything
+else said about PN532 also applies to PN533.
+
+========== ======= ============
+function support remarks
+========== ======= ============
+sense_tta yes
+sense_ttb yes
+sense_ttf yes
+sense_dep yes
+listen_tta yes
+listen_ttb no
+listen_ttf yes Maximimum frame size is 64 byte
+listen_dep yes
+========== ======= ============
+
+"""
+import nfc.clf
+from . import pn53x
+
+import time
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Chipset(pn53x.Chipset):
+ CMD = {
+ # Miscellaneous
+ 0x00: "Diagnose",
+ 0x02: "GetFirmwareVersion",
+ 0x04: "GetGeneralStatus",
+ 0x06: "ReadRegister",
+ 0x08: "WriteRegister",
+ 0x0C: "ReadGPIO",
+ 0x0E: "WriteGPIO",
+ 0x12: "SetParameters",
+ 0x18: "AlparCommandForTDA",
+ # RF Communication
+ 0x32: "RFConfiguration",
+ 0x58: "RFRegulationTest",
+ # Initiator
+ 0x56: "InJumpForDEP",
+ 0x46: "InJumpForPSL",
+ 0x4A: "InListPassiveTarget",
+ 0x50: "InATR",
+ 0x4E: "InPSL",
+ 0x40: "InDataExchange",
+ 0x42: "InCommunicateThru",
+ 0x38: "InQuartetByteExchange",
+ 0x44: "InDeselect",
+ 0x52: "InRelease",
+ 0x54: "InSelect",
+ 0x48: "InActivateDeactivatePaypass",
+ # Target
+ 0x8C: "TgInitAsTarget",
+ 0x92: "TgSetGeneralBytes",
+ 0x86: "TgGetData",
+ 0x8E: "TgSetData",
+ 0x96: "TgSetDataSecure",
+ 0x94: "TgSetMetaData",
+ 0x98: "TgSetMetaDataSecure",
+ 0x88: "TgGetInitiatorCommand",
+ 0x90: "TgResponseToInitiator",
+ 0x8A: "TgGetTargetStatus",
+ }
+ ERR = {
+ 0x01: "Time out, the Target has not answered",
+ 0x02: "Checksum error during RF communication",
+ 0x03: "Parity error during RF communication",
+ 0x04: "Erroneous bit count in anticollision",
+ 0x05: "Framing error during mifare operation",
+ 0x06: "Abnormal bit collision in 106 kbps anticollision",
+ 0x07: "Insufficient communication buffer size",
+ 0x09: "RF buffer overflow detected by CIU",
+ 0x0a: "RF field not activated in time by active mode peer",
+ 0x0b: "Protocol error during RF communication",
+ 0x0d: "Overheated - antenna drivers deactivated",
+ 0x0e: "Internal buffer overflow",
+ 0x10: "Invalid command parameter",
+ 0x12: "Unsupported command from Initiator",
+ 0x13: "Format error during RF communication",
+ 0x14: "Mifare authentication error",
+ 0x18: "Target or Initiator does not support NFC Secure",
+ 0x19: "I2C bus line is busy, a TDA transaction is ongoing",
+ 0x23: "ISO/IEC14443-3 UID check byte is wrong",
+ 0x25: "Command invalid in current DEP state",
+ 0x26: "Operation not allowed in this configuration",
+ 0x27: "Command is not acceptable due to the current context",
+ 0x29: "Released by Initiator while operating as Target",
+ 0x2A: "ISO/IEC14443-3B, the ID of the card does not match",
+ 0x2B: "ISO/IEC14443-3B, card previously activated has disappeared",
+ 0x2C: "NFCID3i and NFCID3t mismatch in DEP 212/424 kbps passive",
+ 0x2D: "An over-current event has been detected",
+ 0x2E: "NAD missing in DEP frame",
+ 0x7f: "Invalid command syntax - received error frame",
+ 0xff: "Insufficient data received from executing chip command",
+ }
+
+ host_command_frame_max_size = 265
+ in_list_passive_target_max_target = 1
+ in_list_passive_target_brty_range = (0, 1, 2, 3, 4, 6, 7, 8)
+
+ def get_general_status(self):
+ data = super(Chipset, self).get_general_status()
+ err = self.ERR.get(data[0], "error code 0x%02X" % data[0])
+ field = ("", "external field detected")[data[1]]
+ if data[2] == 1:
+ br_rx = (106, 212, 424, 848)[data[4]]
+ br_tx = (106, 212, 424, 848)[data[5]]
+ mtype = {0: "A/B", 1: "Active", 2: "Jewel", 16: "FeliCa"}[data[6]]
+ return err, field, (data[3], br_rx, br_tx, mtype)
+ else:
+ return err, field, None
+
+ def _read_register(self, data):
+ data = self.command(0x06, data, timeout=0.25)
+ if data[0] != 0:
+ self.chipset_error(data)
+ return data[1:]
+
+ def _write_register(self, data):
+ data = self.command(0x08, data, timeout=0.25)
+ if data[0] != 0:
+ self.chipset_error(data)
+
+ def tg_init_as_target(self, mode, mifare_params, felica_params,
+ nfcid3t, gt, tk, timeout):
+ assert type(mode) is int and mode & 0b11111100 == 0
+ assert len(mifare_params) == 6
+ assert len(felica_params) == 18
+ assert len(nfcid3t) == 10
+
+ data = (bytearray([mode]) + mifare_params + felica_params + nfcid3t +
+ bytearray([len(gt)]) + gt + bytearray([len(tk)]) + tk)
+ return self.command(0x8c, data, timeout)
+
+
+class Device(pn53x.Device):
+ # Device driver for PN533 based contactless frontends.
+
+ def __init__(self, chipset, logger):
+ assert isinstance(chipset, Chipset)
+ super(Device, self).__init__(chipset, logger)
+
+ ic, ver, rev, support = self.chipset.get_firmware_version()
+ self._chipset_name = "PN5{0:02x}v{1}.{2}".format(ic, ver, rev)
+ self.log.debug("chipset is a {0}".format(self._chipset_name))
+
+ self.mute()
+ self.chipset.rf_configuration(0x02, b"\x00\x0B\x0A")
+ self.chipset.rf_configuration(0x04, b"\x00")
+ self.chipset.rf_configuration(0x05, b"\x01\x00\x01")
+ self.chipset.set_parameters(0b00000000)
+
+ self.eeprom = bytearray()
+ try:
+ self.chipset.read_register(0xA000) # check access
+ for addr in range(0xA000, 0xA100, 64):
+ data = self.chipset.read_register(*range(addr, addr+64))
+ self.eeprom.extend(data)
+ except Chipset.Error:
+ self.log.debug("no eeprom attached")
+
+ if self.eeprom:
+ head = "EEPROM " + ' '.join(["%2X" % i for i in range(16)])
+ self.log.debug(head)
+ for i in range(0, len(self.eeprom), 16):
+ data = ' '.join(["%02X" % x for x in self.eeprom[i:i+16]])
+ self.log.debug(('0x%04X: %s' % (0xA000+i, data)))
+ else:
+ self.log.debug("no eeprom attached")
+
+ self.log.debug("write analog settings for Type A 106 kbps")
+ data = bytearray.fromhex("5A F4 3F 11 4D 85 61 6F 26 62 87")
+ self.chipset.rf_configuration(0x0A, data)
+
+ self.log.debug("write analog settings for Type F 212/424 kbps")
+ data = bytearray.fromhex("6A FF 3F 10 41 85 61 6F")
+ self.chipset.rf_configuration(0x0B, data)
+
+ self.log.debug("write analog settings for Type B 106 kbps")
+ data = bytearray.fromhex("FF 04 85")
+ self.chipset.rf_configuration(0x0C, data)
+
+ self.log.debug("write analog settings for 14443-4 212/424/848 kbps")
+ data = bytearray.fromhex("85 15 8A 85 0A B2 85 04 DA")
+ self.chipset.rf_configuration(0x0D, data)
+
+ def close(self):
+ self.mute()
+ super(Device, self).close()
+
+ def sense_tta(self, target):
+ """Activate the RF field and probe for a Type A Target.
+
+ The PN533 can discover all kinds of Type A Targets (Type 1
+ Tag, Type 2 Tag, and Type 4A Tag) at 106 kbps.
+
+ """
+ return super(Device, self).sense_tta(target)
+
+ def sense_ttb(self, target):
+ """Activate the RF field and probe for a Type B Target.
+
+ The PN533 can discover Type B Targets (Type 4B Tag) at 106,
+ 212, 424, and 848 kbps. The PN533 automatically sends an
+ ATTRIB command that configures a 64 byte maximum frame
+ size. The driver reverts this configuration with a DESELECT
+ and WUPB command to return the target prepared for activation.
+
+ """
+ return super(Device, self).sense_ttb(target)
+
+ def sense_ttf(self, target):
+ """Activate the RF field and probe for a Type F Target.
+
+ The PN533 can discover Type F Targets (Type 3 Tag) at 212 and
+ 424 kbps.
+
+ """
+ return super(Device, self).sense_ttf(target)
+
+ def sense_dep(self, target):
+ """Search for a DEP Target in active communication mode."""
+ return super(Device, self).sense_dep(target)
+
+ def send_cmd_recv_rsp(self, target, data, timeout):
+ """Send command *data* to the remote *target* and return the response
+ data if received within *timeout* seconds.
+
+ """
+ return super(Device, self).send_cmd_recv_rsp(target, data, timeout)
+
+ def _tt1_send_cmd_recv_rsp(self, data, timeout):
+ # Special handling for Tag Type 1 (Jewel/Topaz) card commands.
+
+ if data[0] in (0x00, 0x01, 0x1A, 0x53, 0x72):
+ # RALL, READ, WRITE-NE, WRITE-E, RID are properly
+ # implemented by the PN533 firmware.
+ return self.chipset.in_data_exchange(data, timeout)[0]
+
+ if data[0] == 0x10:
+ # RSEG implementation does not accept any segment other
+ # than 0. Unfortunately we can not directly issue this
+ # command to the CIU because the response is 128 byte and
+ # we're not fast enough to read it from the 64 byte FIFO.
+ rsp = data[1:2]
+ for block in range((data[1] >> 4) * 16, (data[1] >> 4) * 16 + 16):
+ cmd = bytearray([0x02, block]) + data[2:]
+ rsp += self._tt1_send_cmd_recv_rsp(cmd, timeout)[1:9]
+ return rsp
+
+ # Remaining commands READ8, WRITE-E8, WRITE-NE8 are not
+ # implemented by the chipset. Fortunately we can directly
+ # program the CIU through register read/write. Each TT1
+ # command byte must be send as a separate Type A frame, the
+ # first is a short frame with only 7 data bits and the others
+ # are normal frames. Reading is also a bit complicated because
+ # for sending we have to disable the parity generator which
+ # means that we will also receive the parity bits, thus 9 bits
+ # received per 8 data bits. And because they are already
+ # reversed in the FIFO we must swap before parity removal and
+ # afterwards (maybe this could be a bit more optimized).
+ data = self.add_crc_b(data)
+ self.chipset.write_register(
+ ("CIU_FIFOData", data[0]), # CMD_CODE
+ ("CIU_ManualRCV", 0x10), # ParityDisable
+ ("CIU_BitFraming", 0x07), # 7 bits
+ ("CIU_Command", 0x04), # Transmit
+ )
+ for i in range(1, len(data)-1):
+ self.chipset.write_register(
+ ("CIU_FIFOData", data[i]), # CMD_DATA
+ ("CIU_BitFraming", 0x00), # 8 bits
+ ("CIU_Command", 0x04), # Transmit
+ )
+ self.chipset.write_register(
+ ("CIU_FIFOData", data[-1]), # CMD_DATA
+ ("CIU_Command", 0x0C), # Transceive
+ ("CIU_BitFraming", 0x80), # 8 bits, start send
+ )
+ if data[0] == 0x54: # WRITE-E8
+ time.sleep(0.006) # assuming same response time as WRITE-E
+ if data[0] == 0x1B: # WRITE-NE8
+ time.sleep(0.003) # assuming same response time as WRITE-NE
+ self.chipset.write_register(("CIU_ManualRCV", 0x00)) # enable parity
+ fifo_level = self.chipset.read_register("CIU_FIFOLevel")
+ if fifo_level == 0:
+ raise nfc.clf.TimeoutError
+ data = self.chipset.read_register(*(fifo_level * ["CIU_FIFOData"]))
+ data = ''.join(["{:08b}".format(octet)[::-1] for octet in data])
+ data = [int(data[i:i+8][::-1], 2) for i in range(0, len(data)-8, 9)]
+ if self.check_crc_b(data) is False:
+ raise nfc.clf.TransmissionError("crc_b check error")
+ return bytearray(data[:-2])
+
+ def listen_tta(self, target, timeout):
+ """Listen *timeout* seconds for a Type A activation at 106 kbps. The
+ ``sens_res``, ``sdd_res``, and ``sel_res`` response data must
+ be provided and ``sdd_res`` must be a 4 byte UID that starts
+ with ``08h``. Depending on ``sel_res`` an activation may
+ return a target with a ``tt2_cmd``, ``tt4_cmd`` or ``atr_req``
+ attribute. The default RATS response sent for a Type 4 Tag
+ activation can be replaced with a ``rats_res`` attribute.
+
+ """
+ return super(Device, self).listen_tta(target, timeout)
+
+ def listen_ttb(self, target, timeout):
+ """Listen as Type B Target is not supported."""
+ info = "{device} does not support listen as Type B Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def listen_ttf(self, target, timeout):
+ """Listen *timeout* seconds for a Type F card activation. The target
+ ``brty`` must be set to either 212F or 424F and ``sensf_res``
+ provide 19 byte response data (response code + 8 byte IDm + 8
+ byte PMm + 2 byte system code). Note that the maximum command
+ an response frame length is 64 bytes only (including the frame
+ length byte), because the driver must directly program the
+ contactless interface unit within the PN533.
+
+ """
+ return super(Device, self).listen_ttf(target, timeout)
+
+ def listen_dep(self, target, timeout):
+ """Listen *timeout* seconds to become initialized as a DEP Target.
+
+ The PN533 can be set to listen as a DEP Target for passive and
+ active communication mode.
+
+ """
+ return super(Device, self).listen_dep(target, timeout)
+
+ def send_rsp_recv_cmd(self, target, data, timeout):
+ """While operating as *target* send response *data* to the remote
+ device and return new command data if received within
+ *timeout* seconds.
+
+ """
+ return super(Device, self).send_rsp_recv_cmd(target, data, timeout)
+
+ def _init_as_target(self, mode, tta_params, ttf_params, timeout):
+ nfcid3t = ttf_params[0:8] + b"\x00\x00"
+ args = (mode, tta_params, ttf_params, nfcid3t, b'', b'', timeout)
+ return self.chipset.tg_init_as_target(*args)
+
+
+def init(transport):
+ # write ack to perform a soft reset, raises IOError(EACCES) if
+ # someone else has already claimed the USB device.
+ transport.write(Chipset.ACK)
+
+ chipset = Chipset(transport, logger=log)
+ device = Device(chipset, logger=log)
+
+ # PN533 bug: Manufacturer and product strings are no longer
+ # accessible from USB device description after first use with
+ # slightly larger command frames. Better read it from EEPROM.
+ if device.eeprom:
+ index = 0
+ while index < len(device.eeprom) and device.eeprom[index] != 0xFF:
+ tlv_tag, tlv_len = device.eeprom[index], device.eeprom[index+1]
+ tlv_data = device.eeprom[index+2:index+2+tlv_len]
+ if tlv_tag == 3:
+ device._device_name = tlv_data[2:].decode("utf-16-le")
+ if tlv_tag == 4:
+ device._vendor_name = tlv_data[2:].decode("utf-16-le")
+ index += 2 + tlv_len
+ else:
+ device._vendor_name = "SensorID"
+ device._device_name = "StickID"
+
+ return device
diff --git a/src/lib/nfc/clf/pn53x.py b/src/lib/nfc/clf/pn53x.py
new file mode 100644
index 0000000..f8495d5
--- /dev/null
+++ b/src/lib/nfc/clf/pn53x.py
@@ -0,0 +1,1064 @@
+# -*- coding: latin-1 -*-
+
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""This is not really a device driver but a base module that
+implements common functionality for the PN53x family of contactless
+interface chips, namely the NXP PN531, PN532, PN533 and the Sony
+RC-S956.
+
+"""
+import src.lib.nfc.clf
+from . import device
+
+import os
+import time
+import errno
+from binascii import hexlify
+from struct import pack, unpack
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Chipset(object):
+ SOF = bytearray.fromhex('0000FF')
+ ACK = bytearray.fromhex('0000FF00FF00')
+ REG = {
+ 0x6331: "CIU_Command",
+ 0x6332: "CIU_CommIEn",
+ 0x6333: "CIU_DivIEn",
+ 0x6334: "CIU_CommIRq",
+ 0x6335: "CIU_DivIRq",
+ 0x6336: "CIU_Error",
+ 0x6337: "CIU_Status1",
+ 0x6338: "CIU_Status2",
+ 0x6339: "CIU_FIFOData",
+ 0x633A: "CIU_FIFOLevel",
+ 0x633B: "CIU_WaterLevel",
+ 0x633C: "CIU_Control",
+ 0x633D: "CIU_BitFraming",
+ 0x633E: "CIU_Coll",
+ 0x6301: "CIU_Mode",
+ 0x6302: "CIU_TxMode",
+ 0x6303: "CIU_RxMode",
+ 0x6304: "CIU_TxControl",
+ 0x6305: "CIU_TxAuto",
+ 0x6306: "CIU_TxSel",
+ 0x6307: "CIU_RxSel",
+ 0x6308: "CIU_RxThreshold",
+ 0x6309: "CIU_Demod",
+ 0x630A: "CIU_FelNFC1",
+ 0x630B: "CIU_FelNFC2",
+ 0x630C: "CIU_MifNFC",
+ 0x630D: "CIU_ManualRCV",
+ 0x630E: "CIU_TypeB",
+ 0x630F: "CIU_SerialSpeed",
+ 0x6311: "CIU_CRCResultMSB",
+ 0x6312: "CIU_CRCResultLSB",
+ 0x6313: "CIU_GsNOff",
+ 0x6314: "CIU_ModWidth",
+ 0x6315: "CIU_TxBitPhase",
+ 0x6316: "CIU_RFCfg",
+ 0x6317: "CIU_GsNOn",
+ 0x6318: "CIU_CWGsP",
+ 0x6319: "CIU_ModGsP",
+ 0x631A: "CIU_TMode",
+ 0x631B: "CIU_TPrescaler",
+ 0x631C: "CIU_TReloadHi",
+ 0x631D: "CIU_TReloadLo",
+ 0x631E: "CIU_TCounterHi",
+ 0x631F: "CIU_TCounterLo",
+ 0x6321: "CIU_TestSel1",
+ 0x6322: "CIU_TestSel2",
+ 0x6323: "CIU_TestPinEn",
+ 0x6324: "CIU_TestPinValue",
+ 0x6325: "CIU_TestBus",
+ 0x6326: "CIU_AutoTest",
+ 0x6327: "CIU_Version",
+ 0x6328: "CIU_AnalogTest",
+ 0x6329: "CIU_TestDAC1",
+ 0x632A: "CIU_TestDAC2",
+ 0x632B: "CIU_TestADC",
+ 0x632C: "CIU_RFT1",
+ 0x632D: "CIU_RFT2",
+ 0x632E: "CIU_RFT3",
+ 0x632F: "CIU_RFT4",
+ }
+ REGBYNAME = {v: k for k, v in REG.items()}
+
+ class Error(Exception):
+ def __init__(self, errno, strerr):
+ self.errno, self.strerr = errno, strerr
+
+ def __str__(self):
+ return "Error 0x{0:02X}: {1}".format(self.errno, self.strerr)
+
+ def chipset_error(self, cause):
+ if cause is None:
+ errno = 0xff
+ elif type(cause) is int:
+ errno = cause
+ else:
+ errno = cause[0]
+
+ strerr = self.ERR.get(errno, "Unknown error code")
+ raise Chipset.Error(errno, strerr)
+
+ def __init__(self, transport, logger):
+ self.transport = transport
+ self.log = logger
+
+ def close(self):
+ self.transport.close()
+ self.transport = None
+
+ def command(self, cmd_code, cmd_data, timeout):
+ """Send a host command and return the chip response. The chip command
+ is selected by the 8-bit integer *cmd_code*. The command
+ parameters, if any, are supplied with *cmd_data* as a
+ bytearray or byte string. The fully constructed command frame
+ is sent with :meth:`write_frame` and the chip acknowledgement
+ and response is received with :meth:`read_frame`, those
+ methods are used by some drivers for additional framing. The
+ implementation waits 100 ms for the command acknowledgement
+ and then polls every 100 ms for a response frame until
+ *timeout* seconds have elapsed. If the response frame is
+ correct and the response code matches *cmd_code* the data
+ bytes that follow the response code are returned as a
+ bytearray (without the trailing checksum and postamble).
+
+ **Exceptions**
+
+ * :exc:`~exceptions.IOError` :const:`errno.ETIMEDOUT` if no
+ response frame was received before *timeout* seconds.
+
+ * :exc:`~exceptions.IOError` :const:`errno.EIO` if response
+ frame errors were detected.
+
+ * :exc:`Chipset.Error` if an error response frame or status
+ error was received.
+
+ """
+ if cmd_data is not None:
+ assert len(cmd_data) <= self.host_command_frame_max_size - 2
+ self.log.log(logging.DEBUG-1, "{} {} {:.3f}".format(
+ self.CMD[cmd_code], hexlify(cmd_data).decode(), timeout))
+
+ if len(cmd_data) < 254:
+ head = self.SOF + bytearray([len(cmd_data)+2]) \
+ + bytearray([254-len(cmd_data)])
+ else:
+ head = self.SOF + b'\xFF\xFF' + pack(">H", len(cmd_data)+2)
+ head.append((256 - sum(head[-2:])) & 0xFF)
+
+ data = bytearray([0xD4, cmd_code]) + cmd_data
+ tail = bytearray([(256 - sum(data)) & 0xFF, 0])
+
+ try:
+ self.write_frame(head + data + tail)
+ frame = self.read_frame(timeout=100)
+ except IOError:
+ self.log.error("input/output error while waiting for ack")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+
+ if not frame.startswith(self.SOF):
+ self.log.error("invalid frame start sequence")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+
+ if frame[0:len(self.ACK)] != self.ACK:
+ self.log.warning("missing ack frame")
+ else:
+ frame = self.ACK
+
+ if timeout is not None and timeout <= 0:
+ return
+
+ while frame == self.ACK:
+ try:
+ frame = self.read_frame(int(1000 * timeout))
+ except IOError as error:
+ if error.errno == errno.ETIMEDOUT:
+ self.write_frame(self.ACK) # cancel command
+ time.sleep(0.001)
+ raise error
+
+ if frame.startswith(self.SOF + b'\xFF\xFF'):
+ # extended frame
+ if sum(frame[5:8]) & 0xFF != 0:
+ self.log.error("frame lenght checksum error")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+ if unpack(">H", memoryview(frame[5:7]))[0] != len(frame) - 10:
+ self.log.error("frame lenght value mismatch")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+ del frame[0:8]
+ elif frame.startswith(self.SOF):
+ # normal frame
+ if sum(frame[3:5]) & 0xFF != 0:
+ self.log.error("frame lenght checksum error")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+ if frame[3] != len(frame) - 7:
+ self.log.error("frame lenght value mismatch")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+ del frame[0:5]
+ else:
+ self.log.debug("invalid frame start sequence")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+
+ if not sum(frame) & 0xFF == 0:
+ self.log.error("frame data checksum error")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+
+ if frame[0] == 0x7F: # error frame
+ self.chipset_error(0x7F)
+
+ if not frame[0] == 0xD5:
+ self.log.error("invalid frame identifier")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+
+ if not frame[1] == cmd_code + 1:
+ self.log.error("unexpected response code")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+
+ return frame[2:-2]
+
+ def write_frame(self, frame):
+ """Write a command *frame* to the chipset."""
+ self.transport.write(frame)
+
+ def read_frame(self, timeout):
+ """Wait *timeout* milliseconds to return a chip response frame."""
+ return self.transport.read(timeout)
+
+ def send_ack(self):
+ # Send an ACK frame, usually to terminate most recent command.
+ self.transport.write(Chipset.ACK)
+
+ def diagnose(self, test, test_data=None):
+ """Send a Diagnose command. The *test* argument selects the diagnose
+ function either by number or the string ``line``, ``rom``, or
+ ``ram``. For a ``line`` test the implementation sends the
+ longest possible command frame and verifies that the response
+ data is identical. For a ``ram`` or ``rom`` test the
+ implementation verfies the response status. For a *test*
+ number the implementation appends the byte string *test_data*
+ and returns the response data bytes.
+
+ """
+ if test == "line":
+ size = self.host_command_frame_max_size - 3
+ data = b'\x00' + bytearray([x & 0xFF for x in range(size)])
+ return self.command(0x00, data, timeout=1.0) == data
+ if test == "rom":
+ data = self.command(0x00, b'\x01', timeout=1.0)
+ return data and data[0] == 0
+ if test == "ram":
+ data = self.command(0x00, b'\x02', timeout=1.0)
+ return data and data[0] == 0
+ return self.command(0x00, pack('B', test) + test_data, timeout=1.0)
+
+ def get_firmware_version(self):
+ """Send a GetFirmwareVersion command and return the response data
+ bytes.
+
+ """
+ return self.command(0x02, b'', timeout=0.1)
+
+ def get_general_status(self):
+ """Send a GetGeneralStatus command and return the response data
+ bytes.
+
+ """
+ data = self.command(0x04, b'', timeout=0.1)
+ if data is None or len(data) < 3:
+ raise self.chipset_error(None)
+ return data
+
+ def read_register(self, *args):
+ """Send a ReadRegister command for the positional register address or
+ name arguments. The register values are returned as a list for
+ multiple arguments or an integer for a single argument. ::
+
+ tx_mode = Chipset.read_register(0x6302)
+ rx_mode = Chipset.read_register("CIU_RxMode")
+ tx_mode, rx_mode = Chipset.read_register("CIU_TxMode", "CIU_RxMode")
+
+ """
+ def addr(r):
+ return self.REGBYNAME[r] if type(r) is str else r
+
+ args = [addr(reg) for reg in args]
+ data = b''.join([pack(">H", reg) for reg in args])
+ data = self._read_register(data)
+ return list(data) if len(data) > 1 else data[0]
+
+ def _read_register(self, data):
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError(cname + "._read_register")
+
+ def write_register(self, *args):
+ """Send a WriteRegister command. Each positional argument must be an
+ (address, value) tuple except if exactly two arguments are
+ supplied as register address and value. A register can also be
+ selected by name. There is no return value. ::
+
+ Chipset.write_register(0x6301, 0x00)
+ Chipset.write_register("CIU_Mode", 0x00)
+ Chipset.write_register((0x6301, 0x00), ("CIU_TxMode", 0x00))
+
+ """
+ def addr(r):
+ return self.REGBYNAME[r] if type(r) is str else r
+
+ assert type(args) in (tuple, list)
+ if len(args) == 2 and type(args[1]) == int:
+ args = [args]
+ args = [(addr(reg), val) for reg, val in args]
+ data = b''.join([pack(">HB", reg, val) for reg, val in args])
+ self._write_register(data)
+
+ def _write_register(self, data):
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError(cname + "._write_register")
+
+ def set_parameters(self, flags):
+ """Send a SetParameters command with the 8-bit *flags* integer."""
+ self.command(0x12, bytearray([flags]), timeout=0.1)
+
+ def rf_configuration(self, cfg_item, cfg_data):
+ """Send an RFConfiguration command."""
+ self.command(0x32, bytearray([cfg_item]) + bytearray(cfg_data),
+ timeout=0.1)
+
+ def in_jump_for_dep(self, act_pass, br, passive_data, nfcid3, gi):
+ """Send an InJumpForDEP command.
+
+ """
+ assert act_pass in (False, True)
+ assert br in (106, 212, 424)
+ assert len(passive_data) in (0, 4, 5)
+ assert len(nfcid3) in (0, 10)
+ assert len(gi) <= 48
+ cm = int(bool(act_pass))
+ br = (106, 212, 424).index(br)
+ nf = (bool(passive_data) | bool(nfcid3) << 1 | bool(gi) << 2)
+ data = bytearray([cm, br, nf]) + passive_data + nfcid3 + gi
+ data = self.command(0x56, bytearray(data), timeout=3.0)
+ if data is None or data[0] != 0:
+ self.chipset_error(data)
+ return data[2:]
+
+ def in_jump_for_psl(self, act_pass, br, passive_data, nfcid3, gi):
+ """Send an InJumpForPSL command.
+
+ """
+ assert act_pass in (False, True)
+ assert br in (106, 212, 424)
+ assert len(passive_data) in (0, 4, 5)
+ assert len(nfcid3) in (0, 10)
+ assert len(gi) <= 48
+ cm = int(bool(act_pass))
+ br = (106, 212, 424).index(br)
+ nf = (bool(passive_data) | bool(nfcid3) << 1 | bool(gi) << 2)
+ data = bytearray([cm, br, nf]) + passive_data + nfcid3 + gi
+ data = self.command(0x46, data, timeout=3.0)
+ if data is None or data[0] != 0:
+ self.chipset_error(data)
+ return data[2:]
+
+ def in_list_passive_target(self, max_tg, brty, initiator_data):
+ assert max_tg <= self.in_list_passive_target_max_target
+ assert brty in self.in_list_passive_target_brty_range
+ data = bytearray([1, brty]) + initiator_data
+ data = self.command(0x4A, data, timeout=1.0)
+ return data[2:] if data and data[0] > 0 else None
+
+ def in_atr(self, nfcid3i=b'', gi=b''):
+ flag = int(bool(nfcid3i)) | (int(bool(gi)) << 1)
+ data = bytearray([1, flag]) + nfcid3i + gi
+ data = self.command(0x50, data, timeout=1.5)
+ if data is None or data[0] != 0:
+ self.chipset_error(data)
+ return data[1:]
+
+ def in_psl(self, br_it, br_ti):
+ data = bytearray([1, br_it, br_ti])
+ data = self.command(0x4E, data, timeout=1.0)
+ if data is None or data[0] != 0:
+ self.chipset_error(data)
+
+ def in_data_exchange(self, data, timeout, more=False):
+ data = self.command(0x40, bytearray([int(more) << 6 | 0x01]) + data,
+ timeout)
+ if data is None or data[0] & 0x3f != 0:
+ self.chipset_error(data[0] & 0x3f if data else None)
+ return data[1:], bool(data[0] & 0x40)
+
+ def in_communicate_thru(self, data, timeout):
+ data = self.command(0x42, data, timeout)
+ if timeout > 0:
+ if data and data[0] == 0:
+ return data[1:]
+ else:
+ self.chipset_error(data)
+
+ def tg_set_general_bytes(self, gb):
+ data = self.command(0x92, gb, timeout=0.1)
+ if data is None or data[0] != 0:
+ self.chipset_error(data)
+
+ def tg_get_data(self, timeout):
+ data = self.command(0x86, b'', timeout)
+ if data is None or data[0] & 0x3f != 0:
+ self.chipset_error(data[0] & 0x3f if data else None)
+ return data[1:], bool(data[0] & 0x40)
+
+ def tg_set_data(self, data, timeout):
+ data = self.command(0x8E, data, timeout)
+ if data is None or data[0] != 0:
+ self.chipset_error(data)
+
+ def tg_set_meta_data(self, data, timeout):
+ data = self.command(0x94, data, timeout)
+ if data is None or data[0] != 0:
+ self.chipset_error(data)
+
+ def tg_get_initiator_command(self, timeout):
+ data = self.command(0x88, b'', timeout)
+ if timeout > 0:
+ if data and data[0] == 0:
+ return data[1:]
+ else:
+ self.chipset_error(data)
+
+ def tg_response_to_initiator(self, data):
+ data = self.command(0x90, data, timeout=1.0)
+ if data is None or data[0] != 0:
+ self.chipset_error(data)
+
+ def tg_get_target_status(self):
+ data = self.command(0x8A, b'', timeout=0.1)
+ if data[0] == 0x01:
+ br_tx = (106, 212, 424)[data[1] >> 4 & 7]
+ br_rx = (106, 212, 424)[data[1] & 7]
+ else:
+ br_tx, br_rx = (0, 0)
+ return data[0], br_tx, br_rx
+
+
+class Device(device.Device):
+ # Base class for devices with an NXP PN531, PN532, PN533 or Sony
+ # RC-S956 contactless interface chip. This class implements the
+ # functionality that is identical or needed by most of the drivers
+ # that inherit from pn53x.
+
+ def __init__(self, chipset, logger):
+ self.chipset = chipset
+ self.log = logger
+
+ try:
+ chipset_communication = self.chipset.diagnose('line')
+ except Chipset.Error:
+ chipset_communication = False
+
+ if chipset_communication is False:
+ self.log.error("chipset communication test failed")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+
+ # for line in self._print_ciu_register_page(0, 1, 2, 3):
+ # self.log.debug(line)
+
+ # for addr in range(0, 0x03FF, 16):
+ # xram = self.chipset.read_register(*range(addr, addr+16))
+ # xram = ' '.join(["%02X" % x for x in xram])
+ # self.log.debug("0x%04X: %s", addr, xram)
+
+ def close(self):
+ self.chipset.close()
+ self.chipset = None
+
+ def mute(self):
+ self.chipset.rf_configuration(0x01, bytearray([0b00000010]))
+
+ def sense_tta(self, target):
+ brty = {"106A": 0}.get(target.brty)
+ if brty not in self.chipset.in_list_passive_target_brty_range:
+ message = "unsupported bitrate {0}".format(target.brty)
+ self.log.warning(message)
+ raise ValueError(message)
+
+ uid = target.sel_req if target.sel_req else bytearray()
+ if len(uid) > 4:
+ uid = b'\x88' + uid
+ if len(uid) > 8:
+ uid = uid[0:4] + b'\x88' + uid[4:]
+
+ rsp = self.chipset.in_list_passive_target(1, 0, uid)
+ if rsp is not None:
+ sens_res, sel_res, sdd_res = rsp[1::-1], rsp[2:3], rsp[4:]
+ if sel_res[0] & 0x60 == 0x00:
+ self.log.debug("disable crc check for type 2 tag")
+ rxmode = self.chipset.read_register("CIU_RxMode")
+ self.chipset.write_register("CIU_RxMode", rxmode & 0x7F)
+ return src.lib.nfc.clf.RemoteTarget(
+ "106A", sens_res=sens_res, sel_res=sel_res, sdd_res=sdd_res)
+
+ if self.chipset.read_register("CIU_FIFOData") == 0x26:
+ # If we still see the SENS_REQ command in the CIU FIFO
+ # then there was no SENS_RES, thus no tag present.
+ return None
+
+ self.log.debug("sens_res but no sdd_res, try as type 1 tag")
+
+ if 4 not in self.chipset.in_list_passive_target_brty_range:
+ self.log.warning("The {0} can not read Type 1 Tags.".format(self))
+ return None
+
+ rsp = self.chipset.in_list_passive_target(1, 4, b"")
+ if rsp is not None:
+ rid_cmd = bytearray.fromhex("78 0000 00000000")
+ try:
+ rid_res = self.chipset.in_data_exchange(rid_cmd, 0.01)[0]
+ return nfc.clf.RemoteTarget(
+ "106A", sens_res=rsp[1::-1], rid_res=rid_res)
+ except Chipset.Error:
+ pass
+
+ def sense_ttb(self, target, did=None):
+ brty = {"106B": 3, "212B": 6, "424B": 7, "848B": 8}.get(target.brty)
+ if brty not in self.chipset.in_list_passive_target_brty_range:
+ message = "unsupported bitrate {0}".format(target.brty)
+ self.log.warning(message)
+ raise ValueError(message)
+
+ afi = target.sensb_req[0:1] if target.sensb_req else b'\x00'
+ rsp = self.chipset.in_list_passive_target(1, brty, afi)
+ if rsp and rsp[10] & 0b00001001 == 0b00000001:
+ # This is an ISO tag and the chipset has now activated it
+ # with 64-byte max frame size and maybe a DID. Because we
+ # implement ISO-DEP in software and can do without DID and
+ # use a full 256 byte response frame size, we'll send a
+ # DESELECT and WUPB to allow ATTRIB from the activation
+ # code in tags/tt4.py.
+ try:
+ deselect_command = (b'\xCA' + did) if did else b'\xC2'
+ wupb_command = b'\x05' + afi + b'\x08'
+ self.chipset.in_communicate_thru(deselect_command, 0.5)
+ rsp = self.chipset.in_communicate_thru(wupb_command, 0.5)
+ return nfc.clf.RemoteTarget(target.brty, sensb_res=rsp)
+ except (Chipset.Error, IOError) as error:
+ self.log.debug(error)
+
+ def sense_ttf(self, target):
+ brty = {"212F": 1, "424F": 2}.get(target.brty)
+ if brty not in self.chipset.in_list_passive_target_brty_range:
+ message = "unsupported bitrate {0}".format(target.brty)
+ self.log.warning(message)
+ raise ValueError(message)
+
+ if not self.chipset.read_register("CIU_TxControl") & 0b00000011:
+ # Some FeliCa cards need more time from power up to
+ # polling. If the field was not already activated, do this
+ # now and wait about 5 ms.
+ self.chipset.rf_configuration(0x01, b'\x01')
+ time.sleep(0.005)
+
+ default_sensf_req = bytearray.fromhex("00FFFF0100")
+ sensf_req = target.sensf_req if target.sensf_req else default_sensf_req
+ rsp = self.chipset.in_list_passive_target(1, brty, sensf_req)
+ if rsp is not None:
+ return nfc.clf.RemoteTarget(target.brty, sensf_res=rsp[1:])
+
+ def sense_dep(self, target):
+ # Attempt active communication mode target activation.
+ assert target.atr_req, "the target.atr_req attribute is required"
+ assert len(target.atr_req) >= 16, "minimum lenght of atr_req is 16"
+ assert len(target.atr_req) <= 64, "maximum lenght of atr_req is 64"
+
+ # bitrate and modulation type for send/recv must be set and equal
+ assert target.brty_send and target.brty_recv
+ assert target.brty_send == target.brty_recv
+
+ br = int(target.brty[0:-1])
+ nfcid3 = target.atr_req[2:12]
+ gbytes = target.atr_req[16:]
+ try:
+ data = self.chipset.in_jump_for_psl(1, br, b'', nfcid3, gbytes)
+ atr_res = b'\xD5\x01' + data
+ except Chipset.Error as error:
+ if error.errno not in (0x01, 0x0A):
+ self.log.error(error)
+ return None
+ finally:
+ # unset the detect-sync bit, 106A sync byte is handled in dep.py
+ self.chipset.write_register("CIU_Mode", 0b00111011)
+
+ self.log.debug("running DEP in {0} kbps active mode".format(br))
+ return nfc.clf.RemoteTarget(target.brty, atr_res=atr_res,
+ atr_req=target.atr_req)
+
+ def get_max_send_data_size(self, target):
+ return self.chipset.host_command_frame_max_size - 2
+
+ def get_max_recv_data_size(self, target):
+ return self.chipset.host_command_frame_max_size - 3
+
+ def send_cmd_recv_rsp(self, target, data, timeout):
+ def bitrate(brty):
+ return [106 << i for i in range(6)].index(int(brty[:-1]))
+
+ def framing(brty):
+ return {'A': 0b00, 'B': 0b11, 'F': 0b10}[brty[-1:]]
+
+ # Set bitrate and modulation type for send and receive.
+ acm = target.atr_res and not (target.sens_res or target.sensf_res)
+ reg = ("CIU_TxMode", "CIU_RxMode", "CIU_TxAuto")
+ txm, rxm, txa = self.chipset.read_register(*reg)
+ txm = (txm & 0b10001111) | (bitrate(target.brty_send) << 4)
+ rxm = (rxm & 0b10001111) | (bitrate(target.brty_recv) << 4)
+ txm = (txm & 0b11111100) | (0b01 if acm else framing(target.brty_send))
+ rxm = (rxm & 0b11111100) | (0b01 if acm else framing(target.brty_recv))
+ txa = (txa & 0b10111111) | (target.brty_send.endswith("A") << 6)
+ reg = (("CIU_TxMode", txm), ("CIU_RxMode", rxm), ("CIU_TxAuto", txa))
+ self.chipset.write_register(*reg)
+
+ # Calculate the timeout index for InCommunicateThru. The
+ # effective timeout is T(us) = 100 * 2**(n-1) for 1 <= n <= 16
+ # and "no timeout" for n = 0. For a given timeout we calculate
+ # the index as the first effective timeout that is longer.
+ timeout_microsec = int(timeout * 1E6)
+ try:
+ index = [i+1 for i in range(16) if timeout_microsec >> i <= 100][0]
+ except IndexError:
+ index = 16
+ timeout_microsec = 100 << (index-1)
+ timeout = (100 << (index-1)) / 1E6
+ self.log.log(logging.DEBUG-1, "set response timeout %.6f sec", timeout)
+ self.chipset.rf_configuration(0x02, bytearray([10, 11, index]))
+
+ # Send the command data and return the response. All cases
+ # where a response is not received raise either an IOError
+ # or one of the nfc.clf.CommunicationError specializations.
+ data = bytearray(data) if not isinstance(data, bytearray) else data
+ try:
+ if target.sens_res and not target.atr_res:
+ if target.rid_res: # TT1
+ return self._tt1_send_cmd_recv_rsp(data, timeout+0.1)
+ if target.sel_res[0] & 0x60 == 0x00: # TT2
+ return self._tt2_send_cmd_recv_rsp(data, timeout+0.1)
+ return self.chipset.in_communicate_thru(data, timeout+0.1)
+ except Chipset.Error as error:
+ self.log.debug(error)
+ if error.errno == 1:
+ raise src.lib.nfc.clf.TimeoutError
+ else:
+ raise src.lib.nfc.clf.TransmissionError(str(error))
+ except IOError as error:
+ self.log.debug(error)
+ if not error.errno == errno.ETIMEDOUT:
+ raise error
+ else:
+ raise src.lib.nfc.clf.TimeoutError("send_cmd_recv_rsp")
+
+ def _tt1_send_cmd_recv_rsp(self, data, timeout):
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError(cname + "._tt1_send_cmd_recv_rsp()")
+
+ def _tt2_send_cmd_recv_rsp(self, data, timeout):
+ # The Type2Tag implementation needs to receive the Mifare
+ # ACK/NAK responses but the chipset reports them as crc error
+ # (indistinguishable from a real crc error). We thus have to
+ # switch off the crc check and do it here.
+ data = self.chipset.in_communicate_thru(data, timeout)
+ if len(data) > 2 and self.check_crc_a(data) is False:
+ raise nfc.clf.TransmissionError("crc_a check error")
+ return data[:-2] if len(data) > 2 else data
+
+ def listen_tta(self, target, timeout):
+ if target.brty != "106A":
+ info = "unsupported bitrate/type: %r" % target.brty
+ raise nfc.clf.UnsupportedTargetError(info)
+ if target.rid_res:
+ info = "listening for type 1 tag activation is not supported"
+ raise nfc.clf.UnsupportedTargetError(info)
+ try:
+ assert target.sens_res is not None, "sens_res is required"
+ assert target.sdd_res is not None, "sdd_res is required"
+ assert target.sel_res is not None, "sel_res is required"
+ assert len(target.sens_res) == 2, "sens_res must be 2 byte"
+ assert len(target.sdd_res) == 4, "sdd_res must be 4 byte"
+ assert len(target.sel_res) == 1, "sel_res must be 1 byte"
+ assert target.sdd_res[0] == 0x08, "sdd_res[0] must be 08h"
+ except AssertionError as error:
+ raise ValueError(str(error))
+
+ nfcf_params = bytearray(range(18))
+ nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res
+ self.log.debug("nfca_params %s", hexlify(nfca_params).decode())
+
+ # We can use TgInitAsTarget to exclusively answer Type A
+ # activation when the CIU automatic mode detector is disabled
+ # (the firmware does not unset or even check this bit). When
+ # TgInitAsTarget prepares for AutoColl, the firmware also sets
+ # the CIU_TxMode and CIU_RXMode to 106A.
+ self.chipset.write_register("CIU_Mode", 0b00111111)
+
+ time_to_return = time.time() + timeout
+ while time.time() < time_to_return:
+ try:
+ wait = max(time_to_return - time.time(), 0.5)
+ args = (1, nfca_params, nfcf_params, wait)
+ data = self._init_as_target(*args)
+ except IOError as error:
+ if error.errno != errno.ETIMEDOUT:
+ raise error
+ else:
+ return None
+
+ brty = ("106A", "212F", "424F")[(data[0] & 0x70) >> 4]
+ self.log.debug("%s rcvd %s",
+ brty, hexlify(memoryview(data)[1:]).decode())
+ if brty != target.brty or len(data) < 2:
+ log.debug("received bitrate does not match %s", target.brty)
+ continue
+
+ if target.sel_res[0] & 0x60 == 0x00:
+ self.log.debug("rcvd TT2_CMD %s",
+ hexlify(memoryview(data)[1:]).decode())
+ target = nfc.clf.LocalTarget(brty, tt2_cmd=data[1:])
+ target.sens_res = nfca_params[0:2]
+ target.sdd_res = b'\x08' + nfca_params[2:5]
+ target.sel_res = nfca_params[5:6]
+ return target
+
+ elif target.sel_res[0] & 0x20 == 0x20 and data[1] == 0xE0:
+ default_rats_res = bytearray.fromhex("05 78 80 70 02")
+ (rats_cmd, rats_res) = (data[1:], target.rats_res)
+ if not rats_res:
+ rats_res = default_rats_res
+ self.log.debug("rcvd RATS_CMD %s", hexlify(rats_cmd).decode())
+ self.log.debug("send RATS_RES %s", hexlify(rats_res).decode())
+ try:
+ self.chipset.tg_response_to_initiator(rats_res)
+ data = self.chipset.tg_get_initiator_command(1.0)
+ except (Chipset.Error, IOError) as error:
+ self.log.error(error)
+ return
+ if data and data[0] & 0xF0 == 0xC0: # S(DESELECT)
+ self.log.debug("rcvd S(DESELECT) %s",
+ hexlify(data).decode())
+ self.log.debug("send S(DESELECT) %s",
+ hexlify(data).decode())
+ self.chipset.tg_response_to_initiator(data)
+ elif data:
+ self.log.debug("rcvd TT4_CMD %s",
+ hexlify(data).decode())
+ target = nfc.clf.LocalTarget(brty, tt4_cmd=data)
+ target.sens_res = nfca_params[0:2]
+ target.sdd_res = b'\x08' + nfca_params[2:5]
+ target.sel_res = nfca_params[5:6]
+ return target
+
+ elif (target.sel_res[0] & 0x40 and data[1] == 0xF0
+ and len(data) >= 19 and data[2] == len(data)-2
+ and data[3:5] == b'\xD4\x00'):
+ self.log.debug("rcvd ATR_REQ %s",
+ hexlify(memoryview(data)[3:]).decode())
+ target = nfc.clf.LocalTarget(brty, atr_req=data[3:])
+ target.sens_res = nfca_params[0:2]
+ target.sdd_res = b'\x08' + nfca_params[2:5]
+ target.sel_res = nfca_params[5:6]
+ return target
+
+ def listen_ttf(self, target, timeout):
+ # For NFC-F listen we can not use TgInitAsTarget because it
+ # always sets CIU_TxMode and CIU_RxMode to 106A. Best we can
+ # do is to program the CIU AutoColl command and then work with
+ # the CIU to receive tag commands in _tt3_send_rsp_recv_cmd
+ # (InCommunicateThru does not work probably because the
+ # firmware is not in target state). With the 64-bit only CIU
+ # FIFO it means that a tag can only allow two blocks for read
+ # and write.
+ if target.brty not in ("212F", "424F"):
+ info = "unsupported bitrate/type: %r" % target.brty
+ raise nfc.clf.UnsupportedTargetError(info)
+ try:
+ assert target.sensf_res is not None, "sensf_res is required"
+ assert len(target.sensf_res) == 19, "sensf_res must be 19 byte"
+ except AssertionError as error:
+ raise ValueError(str(error))
+
+ nfca_params = bytearray(6)
+ nfcf_params = bytearray(target.sensf_res[1:])
+ self.log.debug("nfcf_params %s", hexlify(nfcf_params).decode())
+
+ regs = [
+ ("CIU_Command", 0b00000000), # Idle command
+ ("CIU_FIFOLevel", 0b10000000), # clear fifo
+ ]
+ regs.extend(zip(25*["CIU_FIFOData"],
+ nfca_params + nfcf_params + b"\0"))
+ regs.append(("CIU_Command", 0b00000001)) # Configure command
+ self.chipset.write_register(*regs)
+ regs = [
+ ("CIU_Control", 0b00000000), # act as target (b4=0)
+ ("CIU_Mode", 0b00111111), # disable mode detector (b2=1)
+ ("CIU_FelNFC2", 0b10000000), # wait until selected (b7=1)
+ ("CIU_TxMode", 0b10000010 | (int(target.brty[:-1])//212) << 4),
+ ("CIU_RxMode", 0b10001010 | (int(target.brty[:-1])//212) << 4),
+ ("CIU_TxControl", 0b10000000), # disable output on TX1/TX2
+ ("CIU_TxAuto", 0b00100000), # wake up when rf level detected
+ ("CIU_Demod", 0b01100001), # use Q channel, freeze PLL in recv
+ ("CIU_CommIRq", 0b01111111), # clear interrupt request bits
+ ("CIU_DivIRq", 0b01111111), # clear interrupt request bits
+ ("CIU_Command", 0b00001101), # AutoColl command
+ ]
+ self.chipset.write_register(*regs)
+
+ regs = ("CIU_Status1", "CIU_Status2", "CIU_CommIRq", "CIU_DivIRq")
+ time_to_return = time.time() + timeout
+ while time.time() < time_to_return:
+ time.sleep(0.01)
+ status1, status2, commirq, divirq \
+ = self.chipset.read_register(*regs)
+ if commirq & 0b00110000 == 0b00110000:
+ self.chipset.write_register("CIU_CommIRq", 0b00110000)
+ fifo_size = self.chipset.read_register("CIU_FIFOLevel")
+ fifo_read = fifo_size * ["CIU_FIFOData"]
+ fifo_data = bytearray(self.chipset.read_register(*fifo_read))
+ if fifo_data and len(fifo_data) == fifo_data[0]:
+ self.log.debug("%s rcvd %s", target.brty,
+ hexlify(fifo_data).decode())
+ if fifo_data[2:10] == nfcf_params[0:8]:
+ target = nfc.clf.LocalTarget(target.brty)
+ target.sensf_res = b'\x01' + nfcf_params
+ target.tt3_cmd = fifo_data[1:]
+ return target
+ # Restart the AutoColl command.
+ self.chipset.write_register("CIU_Command", 0b00001101)
+ self.chipset.write_register("CIU_Command", 0) # Idle command
+
+ def listen_dep(self, target, timeout):
+ assert target.sensf_res is not None
+ assert target.sens_res is not None
+ assert target.sdd_res is not None
+ assert target.sel_res is not None
+ assert target.atr_res is not None
+
+ nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res
+ nfcf_params = target.sensf_res[1:19]
+ self.log.debug("nfca_params %s", hexlify(nfca_params).decode())
+ self.log.debug("nfcf_params %s", hexlify(nfcf_params).decode())
+ assert len(nfca_params) == 6
+ assert len(nfcf_params) == 18
+
+ # enable the automatic mode detector (b2 <= 0)
+ self.chipset.write_register(
+ ("CIU_Mode", 0b01111011), # b2 - enable mode detector
+ ("CIU_TxMode", 0b10110000), # 848 kbps Type A framing
+ ("CIU_RxMode", 0b10110000)) # 848 kbps Type A framing
+
+ time_to_return = time.time() + timeout
+ while time.time() < time_to_return:
+ try:
+ wait = max(time_to_return - time.time(), 0.5)
+ data = self._init_as_target(2, nfca_params, nfcf_params, wait)
+ except IOError as error:
+ if error.errno != errno.ETIMEDOUT:
+ raise error
+ else:
+ if not (data[1] == len(data)-1 and data[2:4] == b'\xD4\x00'):
+ self.log.debug("expected ATR_REQ but got %s",
+ hexlify(memoryview(data)[1:]).decode())
+ else:
+ break
+ else:
+ return
+
+ brty = ("106A", "212F", "424F")[(data[0] & 0b01110000) >> 4]
+ mode = ("passive", "active")[data[0] & 1]
+ self.log.debug("activated in %s %s communication mode", brty, mode)
+
+ atr_req = data[2:]
+ atr_res = target.atr_res[:]
+ atr_res[12] = atr_req[12] # copy DID
+ activation_params = ((nfca_params if brty == "106A" else nfcf_params)
+ if mode == "passive" else None)
+
+ try:
+ self.log.debug("%s send ATR_RES %s", brty,
+ hexlify(atr_res).decode())
+ data = self._send_atr_response(atr_res, timeout=1.0)
+ except Chipset.Error as error:
+ self.log.error(error)
+ return
+ except IOError as error:
+ if error.errno != errno.ETIMEDOUT:
+ raise
+ self.log.debug(error)
+ return
+
+ psl_req = psl_res = None
+ if data and data.startswith(b'\x06\xD4\x04'):
+ self.log.debug("%s rcvd PSL_REQ %s", brty,
+ hexlify(memoryview(data)[1:]).decode())
+ try:
+ psl_req = data[1:]
+ assert len(psl_req) == 5, "psl_req length mismatch"
+ assert psl_req[2] == atr_req[12], "psl_req has wrong did"
+ except AssertionError as error:
+ log.debug(str(error))
+ return None
+ try:
+ psl_res = b'\xD5\x05' + psl_req[2:3]
+ self.log.debug("%s send PSL_RES %s", brty,
+ hexlify(psl_res).decode())
+ brty = self._send_psl_response(psl_req, psl_res, timeout=0.5)
+ data = self.chipset.tg_get_initiator_command(timeout)
+ except Chipset.Error as error:
+ self.log.error(error)
+ return
+ except IOError as error:
+ if error.errno != errno.ETIMEDOUT:
+ raise
+ self.log.debug(error)
+ return
+
+ if data and data[0] == len(data) and data[1:3] == b'\xD4\x06':
+ # set detect-sync bit to 0, the 106A sync byte is handled by dep.py
+ self.chipset.write_register("CIU_Mode", 0b00111011)
+ # prepare the target description to return, exact content
+ # depends on how we were activated (A or F with or w/o PSL)
+ target = nfc.clf.LocalTarget(brty, dep_req=data[1:])
+ target.atr_req, target.atr_res = atr_req, atr_res
+ if psl_req:
+ target.psl_req = psl_req
+ if psl_res:
+ target.psl_res = psl_res
+ if activation_params == nfca_params:
+ target.sens_res = nfca_params[0:2]
+ target.sdd_res = b'\x08' + nfca_params[2:5]
+ target.sel_res = nfca_params[5:6]
+ if activation_params == nfcf_params:
+ target.sensf_res = b'\x01' + nfcf_params
+ return target
+
+ def _init_as_target(self, mode, tta_params, ttf_params, timeout):
+ cname = self.__class__.__module__ + '.' + self.__class__.__name__
+ raise NotImplementedError(cname + '._init_as_target()')
+
+ def _send_atr_response(self, atr_res, timeout):
+ self.chipset.tg_response_to_initiator(
+ bytearray([len(atr_res)+1]) + atr_res)
+ return self.chipset.tg_get_initiator_command(timeout)
+
+ def _send_psl_response(self, psl_req, psl_res, timeout):
+ dsi = psl_req[3] >> 3 & 0b111
+ dri = psl_req[3] & 0b111
+ rx_mode = self.chipset.read_register("CIU_RxMode")
+ rx_mode = (rx_mode & 0b10001111) | (dsi << 4)
+ if rx_mode & 0b00000011 != 1: # if not active mode
+ rx_mode = (rx_mode & 0b11111100) | ((0, 2)[dsi > 0])
+ self.log.debug("set CIU_RxMode to {:08b}".format(rx_mode))
+ self.chipset.write_register(("CIU_RxMode", rx_mode))
+ self.log.debug("send PSL_RES %s", hexlify(psl_res).decode())
+ data = bytearray([len(psl_res)+1]) + psl_res
+ self.chipset.tg_response_to_initiator(data)
+ tx_mode = self.chipset.read_register("CIU_TxMode")
+ tx_mode = (tx_mode & 0b10001111) | (dri << 4)
+ if tx_mode & 0b00000011 != 1: # if not active mode
+ tx_mode = (tx_mode & 0b11111100) | ((0, 2)[dri > 0])
+ self.log.debug("set CIU_TxMode to {:08b}".format(tx_mode))
+ self.chipset.write_register(("CIU_TxMode", tx_mode))
+ return ("106A", "212F", "424F")[dri]
+
+ def _tt3_send_rsp_recv_cmd(self, target, data, timeout):
+ regs = [
+ ("CIU_FIFOLevel", 0b10000000), # clear fifo read/write pointer
+ ("CIU_CommIRq", 0b01111111), # clear interrupt request bits
+ ("CIU_DivIRq", 0b01111111), # clear interrupt request bits
+ ]
+ if data is not None:
+ regs.extend(zip(len(data)*["CIU_FIFOData"], data))
+ regs.append(("CIU_BitFraming", 0b10000000)) # StartSend (b7=1)
+ self.chipset.write_register(*regs)
+
+ irq_regs = ("CIU_CommIRq", "CIU_DivIRq")
+ time_to_return = time.time() + (timeout if timeout else 0)
+ while timeout is None or time.time() < time_to_return:
+ time.sleep(0.01)
+ commirq, divirq = self.chipset.read_register(*irq_regs)
+ if divirq & 0b00000001:
+ raise nfc.clf.BrokenLinkError("external field switched off")
+ if commirq & 0b00100000:
+ self.chipset.write_register("CIU_CommIRq", 0b00100000)
+ fifo_size = self.chipset.read_register("CIU_FIFOLevel")
+ fifo_read = fifo_size * ["CIU_FIFOData"]
+ fifo_data = bytearray(self.chipset.read_register(*fifo_read))
+ if fifo_data[0] != len(fifo_data):
+ raise nfc.clf.TransmissionError("frame length byte error")
+ return fifo_data
+ if timeout > 0:
+ info = "no data received within %.3f s" % timeout
+ self.log.debug(info)
+ raise nfc.clf.TimeoutError(info)
+
+ def send_rsp_recv_cmd(self, target, data, timeout):
+ # print("\n".join(self._print_ciu_register_page(0, 1)))
+ if target.tt3_cmd:
+ return self._tt3_send_rsp_recv_cmd(target, data, timeout)
+ try:
+ if data:
+ self.chipset.tg_response_to_initiator(data)
+ return self.chipset.tg_get_initiator_command(timeout)
+ except Chipset.Error as error:
+ if error.errno in (0x0A, 0x29, 0x31):
+ self.log.debug("Error: %s", error)
+ raise nfc.clf.BrokenLinkError(str(error))
+ else:
+ self.log.warning(error)
+ raise nfc.clf.TransmissionError(str(error))
+ except IOError as error:
+ if error.errno == errno.ETIMEDOUT:
+ info = "no data received within %.3f s" % timeout
+ self.log.debug(info)
+ raise nfc.clf.TimeoutError(info)
+ else:
+ # host-controller communication broken
+ self.log.error(error)
+ raise error
+
+ def _print_ciu_register_page(self, *pages):
+ lines = list()
+ for page in pages:
+ base = (0x6331, 0x6301, 0x6311, 0x6321)[page]
+ regs = set(self.chipset.REG)
+ regs = sorted(regs.intersection(range(base, base+16)))
+ vals = self.chipset.read_register(*regs)
+ regs = [self.chipset.REG[r] for r in regs]
+ for r, v in zip(regs, vals):
+ lines.append("{0:16s} {1:08b}b {2:02X}h".format(r, v, v))
+ return lines
+
+
+def init(transport):
+ log.warning("pn53x is not a driver module, use pn531, pn532, or pn533")
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
diff --git a/src/lib/nfc/clf/rcs380.py b/src/lib/nfc/clf/rcs380.py
new file mode 100644
index 0000000..95fcfe6
--- /dev/null
+++ b/src/lib/nfc/clf/rcs380.py
@@ -0,0 +1,986 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2012, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""Driver module for contactless devices based on the Sony NFC Port-100
+chipset. The only product known to use this chipset is the PaSoRi
+RC-S380. The RC-S380 connects to the host as a native USB device.
+
+The RC-S380 has been the first NFC Forum certified device. It supports
+reading and writing of all NFC Forum tags as well as peer-to-peer
+mode. In addition, the NFC Port-100 also supports card emulation Type
+A and Type F Technology. A notable restriction is that peer-to-peer
+active communication mode (not required for NFC Forum certification)
+is not supported.
+
+========== ======= ============
+function support remarks
+========== ======= ============
+sense_tta yes
+sense_ttb yes
+sense_ttf yes
+sense_dep no
+listen_tta yes Type F responses can not be disabled
+listen_ttb no
+listen_ttf yes
+listen_dep yes Only passive communication mode
+========== ======= ============
+
+"""
+import nfc.clf
+from . import device
+
+import time
+import struct
+import operator
+from functools import reduce
+from binascii import hexlify
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Frame(object):
+ def __init__(self, data):
+ self._data = None
+ self._type = None
+ self._frame = None
+
+ if data[0:3] == bytearray(b"\x00\x00\xff"):
+ frame = bytearray(data)
+ if frame == bytearray(b"\x00\x00\xff\x00\xff\x00"):
+ self._type = "ack"
+ elif frame == bytearray(b"\x00\x00\xFF\xFF\xFF"):
+ self._type = "err"
+ elif frame[3:5] == bytearray(b"\xff\xff"):
+ self._type = "data"
+ if self.type == "data":
+ length = struct.unpack(" 0:
+ data = self.send_command(0x02, data)
+ if data and data[0] != 0:
+ raise StatusError(data[0])
+
+ def in_comm_rf(self, data, timeout):
+ timeout = min((timeout + (1 if timeout > 0 else 0)) * 10, 0xFFFF)
+ data = self.send_command(0x04,
+ struct.pack("Q", data[0:8])[0]
+
+ def set_command_type(self, command_type):
+ data = self.send_command(0x2A, [command_type])
+ if data and data[0] != 0:
+ raise StatusError(data[0])
+
+
+class Device(device.Device):
+ # Device driver for the Sony NFC Port-100 chipset.
+
+ def __init__(self, chipset, logger):
+ self.chipset = chipset
+ self.log = logger
+
+ minor, major = self.chipset.get_firmware_version()
+ self._chipset_name = "NFC Port-100 v{0:x}.{1:02x}".format(major, minor)
+
+ def close(self):
+ self.chipset.close()
+ self.chipset = None
+
+ def mute(self):
+ self.chipset.switch_rf("off")
+
+ def sense_tta(self, target):
+ """Sense for a Type A Target is supported for 106, 212 and 424
+ kbps. However, there may not be any target that understands the
+ activation commands in other than 106 kbps.
+
+ """
+ log.debug("polling for NFC-A technology")
+
+ if target.brty not in ("106A", "212A", "424A"):
+ message = "unsupported bitrate {0}".format(target.brty)
+ raise nfc.clf.UnsupportedTargetError(message)
+
+ self.chipset.in_set_rf(target.brty)
+ self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults)
+ self.chipset.in_set_protocol(initial_guard_time=6, add_crc=0,
+ check_crc=0, check_parity=1,
+ last_byte_bit_count=7)
+
+ sens_req = (target.sens_req if target.sens_req else
+ bytearray.fromhex("26"))
+
+ try:
+ sens_res = self.chipset.in_comm_rf(sens_req, 30)
+ if len(sens_res) != 2:
+ return None
+ except CommunicationError as error:
+ if error != "RECEIVE_TIMEOUT_ERROR":
+ log.debug(error)
+ return None
+
+ log.debug("rcvd SENS_RES %s", hexlify(sens_res).decode())
+
+ if sens_res[0] & 0x1F == 0:
+ log.debug("type 1 tag target found")
+ self.chipset.in_set_protocol(last_byte_bit_count=8, add_crc=2,
+ check_crc=2, type_1_tag_rrdd=2)
+ target = nfc.clf.RemoteTarget(target.brty, sens_res=sens_res)
+ if sens_res[1] & 0x0F == 0b1100:
+ rid_cmd = bytearray.fromhex("78 0000 00000000")
+ log.debug("send RID_CMD %s", hexlify(rid_cmd).decode())
+ try:
+ target.rid_res = self.chipset.in_comm_rf(rid_cmd, 30)
+ except CommunicationError as error:
+ log.debug(error)
+ return None
+ return target
+
+ # other than type 1 tag
+ try:
+ self.chipset.in_set_protocol(last_byte_bit_count=8, add_parity=1)
+ if target.sel_req:
+ uid = target.sel_req
+ if len(uid) > 4:
+ uid = b"\x88" + uid
+ if len(uid) > 8:
+ uid = uid[0:4] + b"\x88" + uid[4:]
+ self.chipset.in_set_protocol(add_crc=1, check_crc=1)
+ for i, sel_cmd in zip(range(0, len(uid), 4), b"\x93\x95\x97"):
+ sel_req = bytearray([sel_cmd, 0x70]) + uid[i:i+4]
+ sel_req.append(reduce(operator.xor, sel_req[2:6])) # BCC
+ log.debug("send SEL_REQ %s", hexlify(sel_req).decode())
+ sel_res = self.chipset.in_comm_rf(sel_req, 30)
+ log.debug("rcvd SEL_RES %s", hexlify(sel_res).decode())
+ uid = target.sel_req
+ else:
+ uid = bytearray()
+ for sel_cmd in b"\x93\x95\x97":
+ self.chipset.in_set_protocol(add_crc=0, check_crc=0)
+ sdd_req = bytearray([sel_cmd, 0x20])
+ log.debug("send SDD_REQ %s", hexlify(sdd_req).decode())
+ sdd_res = self.chipset.in_comm_rf(sdd_req, 30)
+ log.debug("rcvd SDD_RES %s", hexlify(sdd_res).decode())
+ self.chipset.in_set_protocol(add_crc=1, check_crc=1)
+ sel_req = bytearray([sel_cmd, 0x70]) + sdd_res
+ log.debug("send SEL_REQ %s", hexlify(sel_req).decode())
+ sel_res = self.chipset.in_comm_rf(sel_req, 30)
+ log.debug("rcvd SEL_RES %s", hexlify(sel_res).decode())
+ if sel_res[0] & 0b00000100:
+ uid = uid + sdd_res[1:4]
+ else:
+ uid = uid + sdd_res[0:4]
+ break
+ if sel_res[0] & 0b00000100 == 0:
+ return nfc.clf.RemoteTarget(target.brty, sens_res=sens_res,
+ sel_res=sel_res, sdd_res=uid)
+ except CommunicationError as error:
+ log.debug(error)
+
+ def sense_ttb(self, target):
+ """Sense for a Type B Target is supported for 106, 212 and 424
+ kbps. However, there may not be any target that understands the
+ activation command in other than 106 kbps.
+
+ """
+ log.debug("polling for NFC-B technology")
+
+ if target.brty not in ("106B", "212B", "424B"):
+ message = "unsupported bitrate {0}".format(target.brty)
+ raise nfc.clf.UnsupportedTargetError(message)
+
+ self.chipset.in_set_rf(target.brty)
+ self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults)
+ self.chipset.in_set_protocol(initial_guard_time=20, add_sof=1,
+ check_sof=1, add_eof=1, check_eof=1)
+
+ sensb_req = (target.sensb_req if target.sensb_req else
+ bytearray.fromhex("050010"))
+
+ log.debug("send SENSB_REQ %s", hexlify(sensb_req).decode())
+ try:
+ sensb_res = self.chipset.in_comm_rf(sensb_req, 30)
+ except CommunicationError as error:
+ if error != "RECEIVE_TIMEOUT_ERROR":
+ log.debug(error)
+ return None
+
+ if len(sensb_res) >= 12 and sensb_res[0] == 0x50:
+ log.debug("rcvd SENSB_RES %s", hexlify(sensb_res).decode())
+ return nfc.clf.RemoteTarget(target.brty, sensb_res=sensb_res)
+
+ def sense_ttf(self, target):
+ """Sense for a Type F Target is supported for 212 and 424 kbps.
+
+ """
+ log.debug("polling for NFC-F technology")
+
+ if target.brty not in ("212F", "424F"):
+ message = "unsupported bitrate {0}".format(target.brty)
+ raise nfc.clf.UnsupportedTargetError(message)
+
+ self.chipset.in_set_rf(target.brty)
+ self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults)
+ self.chipset.in_set_protocol(initial_guard_time=24)
+
+ sensf_req = (target.sensf_req if target.sensf_req else
+ bytearray.fromhex("00FFFF0100"))
+
+ log.debug("send SENSF_REQ %s", hexlify(sensf_req).decode())
+ try:
+ frame = bytearray([len(sensf_req)+1]) + sensf_req
+ frame = self.chipset.in_comm_rf(frame, 10)
+ except CommunicationError as error:
+ if error != "RECEIVE_TIMEOUT_ERROR":
+ log.debug(error)
+ return None
+
+ if 18 <= len(frame) == frame[0] and frame[1] == 1:
+ log.debug("rcvd SENSF_RES %s", hexlify(frame[1:]).decode())
+ return nfc.clf.RemoteTarget(target.brty, sensf_res=frame[1:])
+
+ def sense_dep(self, target):
+ """Sense for an active DEP Target is not supported. The device only
+ supports passive activation via sense_tta/sense_ttf.
+
+ """
+ message = "{device} does not support sense for active DEP Target"
+ raise nfc.clf.UnsupportedTargetError(message.format(device=self))
+
+ def listen_tta(self, target, timeout):
+ """Listen as Type A Target in 106 kbps.
+
+ Restrictions:
+
+ * It is not possible to send short frames that are required
+ for ACK and NAK responses. This means that a Type 2 Tag
+ emulation can only implement a single sector memory model.
+
+ * It can not be avoided that the chipset responds to SENSF_REQ
+ commands. The driver configures the SENSF_RES response to
+ all zero and ignores all Type F communication but eventually
+ it depends on the remote device whether Type A Target
+ activation will still be attempted.
+
+ """
+ if not target.brty == '106A':
+ info = "unsupported target bitrate: %r" % target.brty
+ raise nfc.clf.UnsupportedTargetError(info)
+
+ if target.rid_res:
+ info = "listening for type 1 tag activation is not supported"
+ raise nfc.clf.UnsupportedTargetError(info)
+
+ if target.sens_res is None:
+ raise ValueError("sens_res is required")
+ if target.sdd_res is None:
+ raise ValueError("sdd_res is required")
+ if target.sel_res is None:
+ raise ValueError("sel_res is required")
+ if len(target.sens_res) != 2:
+ raise ValueError("sens_res must be 2 byte")
+ if len(target.sdd_res) != 4:
+ raise ValueError("sdd_res must be 4 byte")
+ if len(target.sel_res) != 1:
+ raise ValueError("sel_res must be 1 byte")
+ if target.sdd_res[0] != 0x08:
+ raise ValueError("sdd_res[0] must be 08h")
+
+ nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res
+ log.debug("nfca_params %s", hexlify(nfca_params).decode())
+
+ self.chipset.tg_set_rf("106A")
+ self.chipset.tg_set_protocol(self.chipset.tg_set_protocol_defaults)
+ self.chipset.tg_set_protocol(rf_off_error=False)
+
+ time_to_return = time.time() + timeout
+ tg_comm_rf_args = {'mdaa': True, 'nfca_params': nfca_params}
+ tg_comm_rf_args['recv_timeout'] = min(int(1000 * timeout), 0xFFFF)
+
+ def listen_tta_tt2():
+ recv_timeout = tg_comm_rf_args['recv_timeout']
+ while recv_timeout > 0:
+ log.debug("wait %d ms for Type 2 Tag activation", recv_timeout)
+ try:
+ data = self.chipset.tg_comm_rf(**tg_comm_rf_args)
+ except CommunicationError as error:
+ log.debug(error)
+ else:
+ brty = ('106A', '212F', '424F')[data[0]-11]
+ log.debug("%s rcvd %s",
+ brty, hexlify(memoryview(data)[7:]).decode())
+ if brty == "106A" and data[2] & 0x03 == 3:
+ self.chipset.tg_set_protocol(rf_off_error=True)
+ return nfc.clf.LocalTarget(
+ "106A", sens_res=nfca_params[0:2],
+ sdd_res=b'\x08'+nfca_params[2:5],
+ sel_res=nfca_params[5:6], tt2_cmd=data[7:])
+ else:
+ log.debug("not a 106A Type 2 Tag command")
+ finally:
+ recv_timeout = int(1000 * (time_to_return - time.time()))
+ tg_comm_rf_args['recv_timeout'] = recv_timeout
+
+ def listen_tta_tt4():
+ rats_cmd = rats_res = None
+ recv_timeout = tg_comm_rf_args['recv_timeout']
+ while recv_timeout > 0:
+ log.debug("wait %d ms for 106A TT4 command", recv_timeout)
+ try:
+ data = self.chipset.tg_comm_rf(**tg_comm_rf_args)
+ tg_comm_rf_args['transmit_data'] = None
+ except CommunicationError as error:
+ tg_comm_rf_args['transmit_data'] = None
+ rats_cmd = rats_res = None
+ log.debug(error)
+ else:
+ brty = ('106A', '212F', '424F')[data[0]-11]
+ log.debug("%s rcvd %s", brty,
+ hexlify(memoryview(data)[7:]).decode())
+ if brty == "106A" and data[2] == 3 and data[7] == 0xE0:
+ (rats_cmd, rats_res) = (data[7:], target.rats_res)
+ log.debug("rcvd RATS_CMD %s",
+ hexlify(rats_cmd).decode())
+ if rats_res is None:
+ rats_res = bytearray.fromhex("05 78 80 70 02")
+ log.debug("send RATS_RES %s",
+ hexlify(rats_res).decode())
+ tg_comm_rf_args['transmit_data'] = rats_res
+ elif brty == "106A" and data[7] != 0xF0 and rats_cmd:
+ did = rats_cmd[1] & 0x0F
+ cmd = data[7:]
+ ta_tb_tc = rats_res[2:]
+ ta = ta_tb_tc.pop(0) if rats_res[1] & 0x10 else None
+ tb = ta_tb_tc.pop(0) if rats_res[1] & 0x20 else None
+ tc = ta_tb_tc.pop(0) if rats_res[1] & 0x40 else None
+ if ta is not None:
+ log.debug("TA(1) = {:08b}".format(ta))
+ if tb is not None:
+ log.debug("TB(1) = {:08b}".format(tb))
+ if tc is not None:
+ log.debug("TC(1) = {:08b}".format(tc))
+ if ta_tb_tc:
+ log.debug("T({}) = {}".format(
+ len(ta_tb_tc), hexlify(ta_tb_tc).decode()))
+ did_supported = tc is None or bool(tc & 0x02)
+ cmd_with_did = bool(cmd[0] & 0x08)
+ if (((cmd_with_did and did_supported and cmd[1] == did)
+ or (did == 0 and not cmd_with_did))):
+ if cmd[0] in (0xC2, 0xCA):
+ log.debug("rcvd S(DESELECT) %s",
+ hexlify(cmd).decode())
+ tg_comm_rf_args['transmit_data'] = cmd
+ log.debug("send S(DESELECT) %s",
+ hexlify(cmd).decode())
+ rats_cmd = rats_res = None
+ else:
+ log.debug("rcvd TT4_CMD %s",
+ hexlify(cmd).decode())
+ self.chipset.tg_set_protocol(rf_off_error=True)
+ return nfc.clf.LocalTarget(
+ "106A", sens_res=nfca_params[0:2],
+ sdd_res=b'\x08'+nfca_params[2:5],
+ sel_res=nfca_params[5:6], tt4_cmd=cmd,
+ rats_cmd=rats_cmd, rats_res=rats_res)
+ else:
+ log.debug("skip TT4_CMD %s (DID)",
+ hexlify(cmd).decode())
+ else:
+ log.debug("not a 106A TT4 command")
+ finally:
+ recv_timeout = int(1000 * (time_to_return - time.time()))
+ tg_comm_rf_args['recv_timeout'] = recv_timeout
+
+ if target.sel_res[0] & 0x60 == 0x00:
+ return listen_tta_tt2()
+ if target.sel_res[0] & 0x20 == 0x20:
+ return listen_tta_tt4()
+
+ reason = "sel_res does not indicate any tag target support"
+ raise nfc.clf.UnsupportedTargetError(reason)
+
+ def listen_ttb(self, target, timeout):
+ """Listen as Type B Target is not supported."""
+ message = "{device} does not support listen as Type A Target"
+ raise nfc.clf.UnsupportedTargetError(message.format(device=self))
+
+ def listen_ttf(self, target, timeout):
+ """Listen as Type F Target is supported for either 212 or 424 kbps."""
+ if target.brty not in ('212F', '424F'):
+ info = "unsupported target bitrate: %r" % target.brty
+ raise nfc.clf.UnsupportedTargetError(info)
+
+ if target.sensf_res is None:
+ raise ValueError("sensf_res is required")
+ if len(target.sensf_res) != 19:
+ raise ValueError("sensf_res must be 19 byte")
+
+ self.chipset.tg_set_rf(target.brty)
+ self.chipset.tg_set_protocol(self.chipset.tg_set_protocol_defaults)
+ self.chipset.tg_set_protocol(rf_off_error=False)
+
+ recv_timeout = min(int(1000 * timeout), 0xFFFF)
+ time_to_return = time.time() + timeout
+ transmit_data = sensf_req = sensf_res = None
+
+ while recv_timeout > 0:
+ if transmit_data:
+ log.debug("%s send %s", target.brty,
+ hexlify(transmit_data).decode())
+ log.debug("%s wait recv %d ms", target.brty, recv_timeout)
+ try:
+ data = self.chipset.tg_comm_rf(recv_timeout=recv_timeout,
+ transmit_data=transmit_data)
+ except CommunicationError as error:
+ log.debug(error)
+ continue
+ finally:
+ recv_timeout = int((time_to_return - time.time()) * 1E3)
+ transmit_data = None
+
+ assert target.brty == ('106A', '212F', '424F')[data[0]-11]
+ log.debug("%s rcvd %s", target.brty,
+ hexlify(memoryview(data)[7:]).decode())
+
+ if len(data) > 7 and len(data)-7 == data[7]:
+ if sensf_req and data[9:17] == target.sensf_res[1:9]:
+ self.chipset.tg_set_protocol(rf_off_error=True)
+ target = nfc.clf.LocalTarget(target.brty)
+ target.sensf_req = sensf_req
+ target.sensf_res = sensf_res
+ target.tt3_cmd = data[8:]
+ return target
+
+ if len(data) == 13 and data[7] == 6 and data[8] == 0:
+ (sensf_req, sensf_res) = (data[8:], target.sensf_res[:])
+ if (((sensf_req[1] == 255 or sensf_req[1] == sensf_res[17]) and
+ (sensf_req[2] == 255 or sensf_req[2] == sensf_res[18]))):
+ transmit_data = sensf_res[0:17]
+ if sensf_req[3] == 1:
+ transmit_data += sensf_res[17:19]
+ if sensf_req[3] == 2:
+ transmit_data += b"\x00"
+ transmit_data += bytearray(
+ [1 << (target.brty == "424F")])
+ transmit_data = bytearray([len(transmit_data)+1]) \
+ + transmit_data
+
+ def listen_dep(self, target, timeout):
+ log.debug("listen_dep for {0:.3f} sec".format(timeout))
+
+ if not target.sens_res or len(target.sens_res) != 2:
+ raise ValueError("sens_res is required and must be 2 byte")
+ if not target.sel_res or len(target.sel_res) != 1:
+ raise ValueError("sel_res is required and must be 1 byte")
+ if not target.sdd_res or len(target.sdd_res) != 4:
+ raise ValueError("sdd_res is required and must be 4 byte")
+ if not target.sensf_res or len(target.sensf_res) < 19:
+ raise ValueError("sensf_res is required and must be 19 byte")
+ if not target.atr_res or len(target.atr_res) < 17:
+ raise ValueError("atr_res is required and must be >= 17 byte")
+
+ nfca_params = target.sens_res + target.sdd_res[1:4] + target.sel_res
+ nfcf_params = target.sensf_res[1:19]
+ log.debug("nfca_params %s", hexlify(nfca_params).decode())
+ log.debug("nfcf_params %s", hexlify(nfcf_params).decode())
+
+ self.chipset.tg_set_rf("106A")
+ self.chipset.tg_set_protocol(self.chipset.tg_set_protocol_defaults)
+ self.chipset.tg_set_protocol(rf_off_error=False)
+
+ tg_comm_rf_args = {'mdaa': True}
+ tg_comm_rf_args['nfca_params'] = nfca_params
+ tg_comm_rf_args['nfcf_params'] = nfcf_params
+
+ recv_timeout = min(int(1000 * timeout), 0xFFFF)
+ time_to_return = time.time() + timeout
+
+ while recv_timeout > 0:
+ tg_comm_rf_args['recv_timeout'] = recv_timeout
+ log.debug("wait %d ms for activation", recv_timeout)
+ try:
+ data = self.chipset.tg_comm_rf(**tg_comm_rf_args)
+ except CommunicationError as error:
+ if error != "RECEIVE_TIMEOUT_ERROR":
+ log.warning(error)
+ else:
+ brty = ('106A', '212F', '424F')[data[0]-11]
+ log.debug("%s %s", brty, hexlify(data).decode())
+ if data[2] & 0x03 == 3:
+ data = data[7:]
+ break
+ else:
+ log.debug("not a passive mode activation")
+ recv_timeout = int(1000 * (time_to_return - time.time()))
+ else:
+ return None
+
+ # further tg_comm_rf commands return RF_OFF_ERROR when field is gone
+ self.chipset.tg_set_protocol(rf_off_error=True)
+
+ if brty == "106A" and len(data) > 1 and data[0] != 0xF0:
+ # We received a Type A card activation, probably because
+ # sel_res has indicated Type 2 or Type 4A Tag support.
+ target = nfc.clf.LocalTarget("106A", tt2_cmd=data[:])
+ target.sens_res = nfca_params[0:2]
+ target.sdd_res = b'\x08' + nfca_params[2:5]
+ target.sel_res = nfca_params[5:6]
+ return target
+
+ def verify_frame(brty, data, cmd_set):
+ offset = 1 if brty == "106A" else 0
+ try:
+ if brty == "106A" and data[0] != 0xF0:
+ log.warning("rcvd frame has invalid start byte")
+ elif data[offset] != len(data) - offset:
+ log.warning("rcvd frame has incorrect length byte")
+ elif data[offset+1] != 0xD4:
+ log.warning("rcvd frame command byte 1 is not D4h")
+ elif data[offset+2] not in cmd_set:
+ log.warning(
+ "rcvd frame command byte 2 not in %r" % cmd_set)
+ else:
+ return data[offset+1:]
+ except (IndexError):
+ log.warning("rcvd frame with less than header size")
+
+ def send_res_recv_req(brty, data, timeout):
+ if data:
+ data = (b"", b"\xF0")[brty == "106A"] + \
+ bytes([len(data)+1]) + data
+ args = {'transmit_data': data, 'recv_timeout': timeout}
+ data = self.chipset.tg_comm_rf(**args)[7:]
+ if timeout > 0:
+ return verify_frame(brty, data, cmd_set=[0, 4, 6, 8, 10])
+
+ activation_params = nfca_params if brty == '106A' else nfcf_params
+ data = verify_frame(brty, data, cmd_set=[0])
+
+ while data and data[1] == 0:
+ try:
+ (atr_req, atr_res) = (data[:], target.atr_res)
+ log.debug("%s rcvd ATR_REQ %s",
+ brty, hexlify(atr_req).decode())
+ if 16 <= len(atr_req) <= 64:
+ log.debug("%s send ATR_RES %s", brty,
+ hexlify(atr_res).decode())
+ data = send_res_recv_req(brty, atr_res, 1000)
+ else:
+ log.warning("ATR_REQ must be 16 to 64 byte")
+ data = None
+ except (CommunicationError) as error:
+ log.warning(str(error))
+ data = None
+
+ def send_dsl_res(brty, data):
+ dsl_res = b"\xD5\x09" + data[2:3]
+ log.debug("%s send DSL_RES %s", brty, hexlify(dsl_res).decode())
+ send_res_recv_req(brty, dsl_res, 0)
+
+ def send_rls_res(brty, data):
+ rls_res = b"\xD5\x0B" + data[2:3]
+ log.debug("%s send RLS_RES %s", brty, hexlify(rls_res).decode())
+ send_res_recv_req(brty, rls_res, 0)
+
+ def send_psl_res(brty, data):
+ (dsi, dri) = (data[3] >> 3 & 7, data[3] & 7)
+ if dsi != dri:
+ log.error("PSL_REQ DSI != DRI is not supported")
+ raise CommunicationError(b'\0\0\0\0')
+ (psl_req, psl_res) = (data[:], b"\xD5\x05" + data[2:3])
+ log.debug("%s send PSL_RES %s", brty, hexlify(psl_res).decode())
+ send_res_recv_req(brty, psl_res, 0)
+ brty = ('106A', '212F', '424F')[dsi]
+ self.chipset.tg_set_rf(brty)
+ return brty, psl_req, psl_res
+
+ psl_req = None
+ while data and data[1] in (4, 6, 8, 10):
+ did = atr_req[12] if atr_req[12] > 0 else None
+ cmd = {4: "PSL", 6: "DEP", 8: "DSL", 10: "RLS"}.get(data[1], '???')
+ log.debug("%s rcvd %s_REQ %s", brty, cmd, hexlify(data).decode())
+ try:
+ if cmd == "DEP":
+ if did == (data[3] if data[2] >> 2 & 1 else None):
+ target = nfc.clf.LocalTarget(brty, dep_req=data)
+ target.atr_req = atr_req
+ if psl_req:
+ target.psl_req = psl_req
+ if activation_params == nfca_params:
+ target.sens_res = nfca_params[0:2]
+ target.sdd_res = b'\x08' + nfca_params[2:5]
+ target.sel_res = nfca_params[5:6]
+ else:
+ target.sensf_res = b"\x01" + nfcf_params
+ return target
+
+ elif cmd == "DSL":
+ if did == (data[2] if len(data) > 2 else None):
+ return send_dsl_res(brty, data)
+
+ elif cmd == "RLS":
+ if did == (data[2] if len(data) > 2 else None):
+ return send_rls_res(brty, data)
+
+ elif cmd == "PSL": # pragma: no branch
+ if did == (data[2] if data[2] > 0 else None):
+ brty, psl_req, psl_res = send_psl_res(brty, data)
+
+ log.debug("%s wait recv 1000 ms", brty)
+ data = send_res_recv_req(brty, None, 1000)
+
+ except (CommunicationError) as error:
+ log.warning(str(error))
+ return None
+
+ def get_max_send_data_size(self, target):
+ return 290
+
+ def get_max_recv_data_size(self, target):
+ return 290
+
+ def send_cmd_recv_rsp(self, target, data, timeout):
+ if timeout:
+ timeout_msec = max(min(int(timeout * 1000), 0xFFFF), 1)
+ else:
+ timeout_msec = 0
+ self.chipset.in_set_rf(target.brty_send, target.brty_recv)
+ self.chipset.in_set_protocol(self.chipset.in_set_protocol_defaults)
+ in_set_protocol_settings = {}
+ if target.brty_send.endswith('A'):
+ in_set_protocol_settings['add_parity'] = 1
+ in_set_protocol_settings['check_parity'] = 1
+ if target.brty_send.endswith('B'):
+ in_set_protocol_settings['initial_guard_time'] = 20
+ in_set_protocol_settings['add_sof'] = 1
+ in_set_protocol_settings['check_sof'] = 1
+ in_set_protocol_settings['add_eof'] = 1
+ in_set_protocol_settings['check_eof'] = 1
+ try:
+ if ((target.brty == '106A' and target.sel_res and
+ target.sel_res[0] & 0x60 == 0x00)):
+ # Driver must check TT2 CRC to get ACK/NAK
+ in_set_protocol_settings['check_crc'] = 0
+ self.chipset.in_set_protocol(**in_set_protocol_settings)
+ return self._tt2_send_cmd_recv_rsp(data, timeout_msec)
+ else:
+ self.chipset.in_set_protocol(**in_set_protocol_settings)
+ return self.chipset.in_comm_rf(data, timeout_msec)
+ except CommunicationError as error:
+ log.debug(error)
+ if error == "RECEIVE_TIMEOUT_ERROR":
+ raise nfc.clf.TimeoutError
+ raise nfc.clf.TransmissionError
+
+ def _tt2_send_cmd_recv_rsp(self, data, timeout_msec):
+ # The Type2Tag implementation needs to receive the Mifare
+ # ACK/NAK responses but the chipset reports them as crc error
+ # (indistinguishable from a real crc error). We thus had to
+ # switch off the crc check and do it here.
+ data = self.chipset.in_comm_rf(data, timeout_msec)
+ if len(data) > 2 and self.check_crc_a(data) is False:
+ raise nfc.clf.TransmissionError("crc_a check error")
+ return data[:-2] if len(data) > 2 else data
+
+ def send_rsp_recv_cmd(self, target, data, timeout):
+ assert timeout is None or timeout >= 0
+ kwargs = {
+ 'guard_time': 500,
+ 'transmit_data': data,
+ 'recv_timeout': 0xFFFF if timeout is None else int(timeout*1E3),
+ }
+ try:
+ data = self.chipset.tg_comm_rf(**kwargs)
+ return data[7:] if data else None
+ except CommunicationError as error:
+ log.debug(error)
+ if error == "RF_OFF_ERROR":
+ raise nfc.clf.BrokenLinkError(str(error))
+ if error == "RECEIVE_TIMEOUT_ERROR":
+ raise nfc.clf.TimeoutError(str(error))
+ raise nfc.clf.TransmissionError(str(error))
+
+
+def init(transport):
+ chipset = Chipset(transport, logger=log)
+ device = Device(chipset, logger=log)
+ device._vendor_name = transport.manufacturer_name
+ device._device_name = transport.product_name
+ return device
diff --git a/src/lib/nfc/clf/rcs956.py b/src/lib/nfc/clf/rcs956.py
new file mode 100644
index 0000000..7dbde12
--- /dev/null
+++ b/src/lib/nfc/clf/rcs956.py
@@ -0,0 +1,376 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""Driver for contacless devices based on the Sony RC-S956
+chipset. Products known to use this chipset are the PaSoRi RC-S330,
+RC-S360, and RC-S370. The RC-S956 connects to the host as a native USB
+device.
+
+The RC-S956 has the same hardware architecture as the NXP PN53x
+family, i.e. it has a PN512 Contactless Interface Unit (CIU) coupled
+with a 80C51 microcontroller and uses the same frame structure for
+host communication and mostly the same commands. However, the firmware
+that runs on the 80C51 is different and the most notable difference is
+a much stricter state machine. The state machine restricts allowed
+commands to certain modes. While direct access to the CIU registers is
+possible, some of the things that can be done with a PN53x are
+unfortunately prevented by the stricter state machine.
+
+========== ======= ============
+function support remarks
+========== ======= ============
+sense_tta yes Only Type 1 Tags up to 128 byte (Topaz-96)
+sense_ttb yes ATTRIB by firmware voided with S(DESELECT)
+sense_ttf yes
+sense_dep yes
+listen_tta yes Only DEP and Type 2 Target
+listen_ttb no
+listen_ttf no
+listen_dep yes Only passive communication mode
+========== ======= ============
+
+"""
+import nfc.clf
+from . import pn53x
+
+import time
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Chipset(pn53x.Chipset):
+ CMD = {
+ 0x00: "Diagnose",
+ 0x02: "GetFirmwareVersion",
+ 0x04: "GetGeneralStatus",
+ 0x06: "ReadRegister",
+ 0x08: "WriteRegister",
+ 0x0C: "ReadGPIO",
+ 0x10: "SetSerialBaudrate",
+ 0x12: "SetParameters",
+ 0x16: "PowerDown",
+ 0x32: "RFConfiguration",
+ 0x58: "RFRegulationTest",
+ 0x18: "ResetMode",
+ 0x1C: "ControlLED",
+ 0x56: "InJumpForDEP",
+ 0x46: "InJumpForPSL",
+ 0x4A: "InListPassiveTarget",
+ 0x50: "InATR",
+ 0x4E: "InPSL",
+ 0x40: "InDataExchange",
+ 0x42: "InCommunicateThru",
+ 0x44: "InDeselect",
+ 0x52: "InRelease",
+ 0x54: "InSelect",
+ 0x8C: "TgInitTarget",
+ 0x92: "TgSetGeneralBytes",
+ 0x86: "TgGetDEPData",
+ 0x8E: "TgSetDEPData",
+ 0x94: "TgSetMetaDEPData",
+ 0x88: "TgGetInitiatorCommand",
+ 0x90: "TgResponseToInitiator",
+ 0x8A: "TgGetTargetStatus",
+ 0xA0: "CommunicateThruEX",
+ }
+ ERR = {
+ 0x01: "Time out, the Target has not answered",
+ 0x02: "Checksum error during RF communication",
+ 0x03: "Parity error during RF communication",
+ 0x04: "Incorrect collision bit position in TargetID during SDD",
+ 0x07: "Overflow detected by the hardware during RF communication",
+ 0x0A: "RF field not activated in time by active mode peer",
+ 0x0B: "Protocol error during RF communication",
+ 0x0C: "More than 260 bytes payload received in ISO-DEP chaining",
+ 0x0D: "Overheated - antenna drivers deactivated",
+ 0x10: "Size of RF response packet during SDD was more than 4 bytes",
+ 0x13: "Format error during RF communication or retry count exceeded",
+ 0x14: "Authentication A or B failed for Type-A ISO target",
+ 0x17: "Unmatched block number in R(ACK) from ISO Type A or B card",
+ 0x23: "Invalid BCC value from ISO Type A card during anticollision",
+ 0x25: "TgGetDEPData or TgSetDEPData executed at wrong time",
+ 0x26: "PowerDown command received while USB interface being used",
+ 0x27: "Abnormal Tg parameter in the host command packet",
+ 0x29: "Release from the initiator in operation as DEPTarget",
+ 0x2A: "PUPI information in ATQB response differs from initial value",
+ 0x2B: "Failure to select a deselected target",
+ 0x2F: "Already deselected by the initiator in operation as DEPTarget",
+ 0x31: "Initiator RF-OFF state detected while operating as Target",
+ 0x32: "Buffer overflow detected by firmware during RF communication",
+ 0x34: "DEP_REQ(NACK) received but DEP_RES(INF) was never returned",
+ 0x35: "The received data exceeds LEN in the RF packet",
+ 0x7f: "Invalid command syntax - received error frame",
+ 0xfe: "A register write operation failed",
+ 0xff: "No data received from executing chip command",
+ }
+
+ host_command_frame_max_size = 265
+ in_list_passive_target_max_target = 1
+ in_list_passive_target_brty_range = (0, 1, 2, 3, 4)
+
+ def diagnose(self, test, test_data=None):
+ if test == "line":
+ size = self.host_command_frame_max_size - 3
+ data = bytearray([x & 0xFF for x in range(size)])
+ return self.command(0x00, b"\x00" + data, timeout=1.0) == data
+ return super(Chipset, self).diagnose(test, test_data)
+
+ def _read_register(self, data):
+ # Max 64 registers can be read from RCS956
+ assert len(data) <= 128
+ return self.command(0x06, data, timeout=0.25)
+
+ def _write_register(self, data):
+ # Max 64 registers can be written to RCS956
+ assert len(data) <= 192
+ status = self.command(0x08, data, timeout=0.25)
+ if sum(status) != 0:
+ self.chipset_error(0xfe)
+
+ def reset_mode(self):
+ """Send a Reset command to set the operation mode to 0."""
+ self.command(0x18, b"\x01", timeout=0.1)
+ self.transport.write(Chipset.ACK)
+ time.sleep(0.010)
+
+ def tg_init_target(self, mode, mifare_params, felica_params,
+ nfcid3t, gt, timeout):
+ assert type(mode) is int and mode & 0b11111101 == 0
+ assert len(mifare_params) == 6
+ assert len(felica_params) == 18
+ assert len(nfcid3t) == 10
+
+ data = bytearray([mode]) + mifare_params + felica_params + nfcid3t + gt
+ return self.command(0x8c, data, timeout)
+
+
+class Device(pn53x.Device):
+ # Device driver for Sony RC-S956 based contactless devices.
+
+ def __init__(self, chipset, logger):
+ assert isinstance(chipset, Chipset)
+ # Reset the RCS956 state machine to Mode 0. We may have left
+ # it in some other mode when an error has occured.
+ chipset.reset_mode()
+
+ super(Device, self).__init__(chipset, logger)
+
+ ic, ver, rev, support = self.chipset.get_firmware_version()
+ self._chipset_name = "RCS956v{0:x}.{1:x}".format(ver, rev)
+ self.log.debug("chipset is a {0}".format(self._chipset_name))
+
+ self.mute()
+ # Set timeout for PSL_RES, ATR_RES, InDataExchange/InCommunicateThru
+ self.chipset.rf_configuration(0x02, b"\x0B\x0B\x0A")
+ self.chipset.rf_configuration(0x04, b"\x00")
+ self.chipset.rf_configuration(0x05, b"\x00\x00\x01")
+
+ self.log.debug("write rf settings for 106A")
+ data = bytearray.fromhex("5A F4 3F 11 4D 85 61 6F 26 62 87")
+ self.chipset.rf_configuration(0x0A, data)
+
+ self.chipset.set_parameters(0b00001000)
+ self.chipset.reset_mode()
+
+ # Set the RFCfg value for RAM-07. RF settings in RAM-07 are
+ # used for initial target state. During power-up RAM-07 is
+ # loaded from EEPROM-07 and the RFCfg value 0xFD stored in
+ # EEPROM-07 for RC-S330/360 prevents passive mode activation
+ # at 106A. It works with the RFCfg value 0x59 stored in ROM-07
+ # (Neither value makes it work in active mode).
+ self.chipset.write_register(0x0328, 0x59)
+
+ def close(self):
+ self.mute()
+ super(Device, self).close()
+
+ def mute(self):
+ self.chipset.reset_mode()
+ super(Device, self).mute()
+
+ def sense_tta(self, target):
+ """Activate the RF field and probe for a Type A Target.
+
+ The RC-S956 can discover all Type A Targets (Type 1 Tag, Type
+ 2 Tag, and Type 4A Tag) at 106 kbps. Due to firmware
+ restrictions it is not possible to read a Type 1 Tag with
+ dynamic memory layout (more than 128 byte memory).
+
+ """
+ target = super(Device, self).sense_tta(target)
+ if target and target.rid_res:
+ # This is a TT1 tag. Unfortunately we can only read it if
+ # it is a static memory tag. The RCS956 has implemented
+ # the same wrong command codes as PN531/2/3 and directly
+ # programming the CIU does not work.
+ if target.rid_res[0] >> 4 == 1 and target.rid_res[0] & 15 != 1:
+ msg = "The {device} can not read this Type 1 Tag."
+ self.log.warning(msg.format(device=self))
+ return None
+ return target
+
+ def sense_ttb(self, target):
+ """Activate the RF field and probe for a Type B Target.
+
+ The RC-S956 can discover Type B Targets (Type 4B Tag) at 106
+ kbps. For a Type 4B Tag the firmware automatically sends an
+ ATTRIB command that configures the use of DID and 64 byte
+ maximum frame size. The driver reverts this configuration with
+ a DESELECT and WUPB command to return the target prepared for
+ activation (which nfcpy does in the tag activation code).
+
+ """
+ return super(Device, self).sense_ttb(target, did=b'\x01')
+
+ def sense_ttf(self, target):
+ """Activate the RF field and probe for a Type F Target.
+
+ """
+ return super(Device, self).sense_ttf(target)
+
+ def sense_dep(self, target):
+ """Search for a DEP Target in active or passive communication mode.
+
+ """
+ # Set timeout for PSL_RES and ATR_RES
+ self.chipset.rf_configuration(0x02, b"\x0B\x0B\x0A")
+ return super(Device, self).sense_dep(target)
+
+ def listen_tta(self, target, timeout):
+ """Listen *timeout* seconds for a Type A activation at 106 kbps. The
+ ``sens_res``, ``sdd_res``, and ``sel_res`` response data must
+ be provided and ``sdd_res`` must be a 4 byte UID that starts
+ with ``08h``. Depending on ``sel_res`` an activation may
+ return a target with ``tt2_cmd`` or ``atr_req`` attribute. A
+ Type 4A Tag activation is not supported.
+
+ """
+ if target.sel_res and target.sel_res[0] & 0x20:
+ info = "{device} does not support listen as Type 4A Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+ return super(Device, self).listen_tta(target, timeout)
+
+ def listen_ttb(self, target, timeout):
+ """Listen as Type B Target is not supported."""
+ info = "{device} does not support listen as Type B Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def listen_ttf(self, target, timeout):
+ """Listen as Type F Target is not supported."""
+ info = "{device} does not support listen as Type F Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def listen_dep(self, target, timeout):
+ """Listen *timeout* seconds to become initialized as a DEP Target.
+
+ The RC-S956 can be set to listen as a DEP Target for passive
+ communication mode. Target active communication mode is
+ disabled by the driver due to performance issues. It is also
+ not possible to fully control the ATR_RES response, only the
+ response waiting time (TO byte of ATR_RES) and the general
+ bytes can be set by the driver. Because the TO value must be
+ set before calling the hardware listen function, it can not be
+ different for the Type A of Type F passive initalization (the
+ driver uses the higher value if they are different).
+
+ """
+ # The RCS956 internal state machine must be in Mode 0 before
+ # we enter the listen phase. Also the RFConfiguration command
+ # for setting the TO parameter won't work in any other mode.
+ self.chipset.reset_mode()
+
+ # Set the WaitForSelected bit in CIU_FelNFC2 register to
+ # prevent active mode activation. Target active mode is not
+ # really working with this device.
+ self.chipset.write_register("CIU_FelNFC2", 0x80)
+
+ # We can not send ATR_RES as as a regular response but must
+ # use TgSetGeneralBytes to advance the chipset state machine
+ # to mode 3. Thus the ATR_RES is mostly determined by the
+ # firmware, we can only control the TO parameter for RWT, but
+ # must do it before the actual listen.
+ to = target.atr_res[15] & 0x0F
+ self.chipset.rf_configuration(0x82, bytearray([to, 2, to]))
+
+ # Disable automatic ATR_RES transmission. This must be done
+ # all again because the chipset reactivates the setting after
+ # ATR_RES was once send in TgSetGeneralBytes.
+ self.chipset.set_parameters(0b00001000)
+
+ # Now we can use the generic pn53x implementation
+ return super(Device, self).listen_dep(target, timeout)
+
+ def _init_as_target(self, mode, tta_params, ttf_params, timeout):
+ nfcid3t = ttf_params[0:8] + b"\x00\x00"
+ args = (mode & 0xFE, tta_params, ttf_params, nfcid3t, b'', timeout)
+ return self.chipset.tg_init_target(*args)
+
+ def _send_atr_response(self, atr_res, timeout):
+ # Before ATR_RES the device is in Mode 2 which does not allow
+ # the use of TgResponseToInitiator. To send the ATR_RES we
+ # must use TgSetGeneralBytes and can control only the general
+ # bytes and TO which we've set in _listen_dep(). We now copy
+ # the DID value from atr_req to atr_res but this will likely
+ # have no effect on the actual response. The hope is that the
+ # firmware will do the same when sending ATR_RES and we tell
+ # the truth to the caller.
+ self.log.debug("calling TgSetGeneralBytes to send ATR_RES")
+ self.chipset.tg_set_general_bytes(atr_res[17:])
+ return self.chipset.tg_get_initiator_command(timeout)
+
+ def _tt1_send_cmd_recv_rsp(self, data, timeout):
+ # Special handling for Tag Type 1 (Jewel/Topaz) card commands.
+
+ if data[0] in (0x00, 0x01, 0x1A, 0x53, 0x72):
+ # RALL, READ, WRITE-NE, WRITE-E, RID are properly
+ # implemented by firmware.
+ return self.chipset.in_data_exchange(data, timeout)[0]
+
+ # The other commands can not be executed. The workaround found
+ # for PN531, PN532 and PN533 fails with RCS956. While it is
+ # possible to properly send a TT1 command and the tag answers
+ # as expected, there is no way to get the response data from
+ # the CIU FIFO. For whatever reason the FIFO is empty, maybe
+ # the firmware constantly polls for new data and just removes
+ # it. That the response data was received can be guessed from
+ # the fact that the CIU Control register shows has the
+ # RxLastBits field set to exactly the correct number of valid
+ # bits in the last byte (when parity check is disabled,
+ # i.e. the FIFO contains one more bit for each received byte.
+ self.log.debug("tt1 command can not be send with this hardware ")
+ raise nfc.clf.TransmissionError("tt1 command can not be send")
+
+
+def init(transport):
+ # Write ack to see if we can talk to the device. This raises
+ # IOError(EACCES) if it's claimed by some other process.
+ transport.write(Chipset.ACK)
+
+ chipset = Chipset(transport, logger=log)
+ device = Device(chipset, logger=log)
+
+ device._vendor_name = transport.manufacturer_name
+ device._device_name = transport.product_name
+ if device._device_name is None:
+ device._device_name = "RC-S330"
+
+ return device
diff --git a/src/lib/nfc/clf/transport.py b/src/lib/nfc/clf/transport.py
new file mode 100644
index 0000000..b0eef8a
--- /dev/null
+++ b/src/lib/nfc/clf/transport.py
@@ -0,0 +1,345 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2012, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+#
+# Transport layer for host to reader communication.
+#
+import os
+import re
+import errno
+from binascii import hexlify
+
+if not os.getenv("READTHEDOCS"): # pragma: no cover
+ try:
+ import usb1 as libusb
+ except ImportError: # pragma: no cover
+ raise ImportError("missing usb1 module, try 'pip install libusb1'")
+
+try:
+ import serial
+ import serial.tools.list_ports
+except ImportError: # pragma: no cover
+ raise ImportError("missing serial module, try 'pip install pyserial'")
+
+try:
+ import termios
+except ImportError: # pragma: no cover
+ assert os.name != 'posix'
+
+import logging
+log = logging.getLogger(__name__)
+
+PATH = re.compile(r'^([a-z]+)(?::|)([a-zA-Z0-9-]+|)(?::|)([a-zA-Z0-9]+|)$')
+
+
+class TTY(object):
+ TYPE = "TTY"
+
+ @classmethod
+ def find(cls, path):
+ if not (path.startswith("tty") or path.startswith("com")):
+ return
+
+ match = PATH.match(path)
+
+ if match and match.group(1) == "tty":
+ if re.match(r'^(S|ACM|AMA|USB)\d+$', match.group(2)):
+ TTYS = re.compile(r'^tty{}$'.format(match.group(2)))
+ glob = False
+ elif re.match(r'^(S|ACM|AMA|USB)$', match.group(2)):
+ TTYS = re.compile(r'^tty{}\d+$'.format(match.group(2)))
+ glob = True
+ elif re.match(r'^usbserial-\w+$', match.group(2)):
+ TTYS = re.compile(r'^cu\.{}$'.format(match.group(2)))
+ glob = False
+ elif re.match(r'^usbserial$', match.group(2)):
+ TTYS = re.compile(r'^cu\.usbserial-.*$')
+ glob = True
+ elif re.match(r'^.+$', match.group(2)):
+ TTYS = re.compile(r'^{}$'.format(match.group(2)))
+ glob = False
+ else:
+ TTYS = re.compile(r'^(tty(S|ACM|AMA|USB)\d+|cu\.usbserial.*)$')
+ glob = True
+
+ log.debug(TTYS.pattern)
+ ttys = [fn for fn in os.listdir('/dev') if TTYS.match(fn)]
+
+ if len(ttys) > 0:
+ # Sort ttys with custom function to correctly order numbers.
+ ttys.sort(key=lambda item: (len(item), item))
+ log.debug('check: ' + ' '.join('/dev/' + tty for tty in ttys))
+
+ # Eliminate tty nodes that are not physically present or
+ # inaccessible by the current user. Propagate IOError when
+ # path designated exactly one device, otherwise just log.
+ for i, tty in enumerate(ttys):
+ try:
+ termios.tcgetattr(open('/dev/%s' % tty))
+ ttys[i] = '/dev/%s' % tty
+ except termios.error:
+ pass
+ except IOError as error:
+ log.debug(error)
+ if not glob:
+ raise error
+
+ ttys = [tty for tty in ttys if tty.startswith('/dev/')]
+ log.debug('avail: %s', ' '.join([tty for tty in ttys]))
+ return ttys, match.group(3), glob
+
+ if match and match.group(1) == "com":
+ if re.match(r'^COM\d+$', match.group(2)):
+ return [match.group(2)], match.group(3), False
+ if re.match(r'^\d+$', match.group(2)):
+ return ["COM" + match.group(2)], match.group(3), False
+ if re.match(r'^$', match.group(2)):
+ ports = [p[0] for p in serial.tools.list_ports.comports()]
+ log.debug('serial ports: %s', ' '.join(ports))
+ return ports, match.group(3), True
+ log.error("invalid port in 'com' path: %r", match.group(2))
+
+ @property
+ def manufacturer_name(self):
+ return None
+
+ @property
+ def product_name(self):
+ return None
+
+ def __init__(self, port=None):
+ self.tty = None
+ self.open(port)
+
+ def open(self, port, baudrate=115200):
+ self.close()
+ self.tty = serial.Serial(port, baudrate, timeout=0.05)
+
+ @property
+ def port(self):
+ return self.tty.port if self.tty else ''
+
+ @property
+ def baudrate(self):
+ return self.tty.baudrate if self.tty else 0
+
+ @baudrate.setter
+ def baudrate(self, value):
+ if self.tty:
+ self.tty.baudrate = value
+
+ def read(self, timeout):
+ if self.tty is not None:
+ self.tty.timeout = max(timeout/1E3, 0.05)
+ frame = bytearray(self.tty.read(6))
+ if frame is None or len(frame) == 0:
+ raise IOError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT))
+ if frame.startswith(b"\x00\x00\xff\x00\xff\x00"):
+ log.log(logging.DEBUG-1, "<<< %s", hexlify(frame).decode())
+ return frame
+ LEN = frame[3]
+ if LEN == 0xFF:
+ frame += self.tty.read(3)
+ LEN = frame[5] << 8 | frame[6]
+ frame += self.tty.read(LEN + 1)
+ log.log(logging.DEBUG-1, "<<< %s", hexlify(frame).decode())
+ return frame
+
+ def write(self, frame):
+ if self.tty is not None:
+ log.log(logging.DEBUG-1, ">>> %s", hexlify(frame).decode())
+ self.tty.flushInput()
+ try:
+ self.tty.write(frame)
+ except serial.SerialTimeoutException:
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+
+ def close(self):
+ if self.tty is not None:
+ self.tty.flushOutput()
+ self.tty.close()
+ self.tty = None
+
+
+class USB(object):
+ TYPE = "USB"
+
+ @classmethod
+ def find(cls, path):
+ if not path.startswith("usb"):
+ return
+
+ log.debug("using libusb-{0}.{1}.{2}".format(*libusb.getVersion()[0:3]))
+
+ usb_or_none = re.compile(r'^(usb|)$')
+ usb_vid_pid = re.compile(r'^usb(:[0-9a-fA-F]{4})(:[0-9a-fA-F]{4})?$')
+ usb_bus_dev = re.compile(r'^usb(:[0-9]{1,3})(:[0-9]{1,3})?$')
+ match = None
+
+ for regex in (usb_vid_pid, usb_bus_dev, usb_or_none):
+ m = regex.match(path)
+ if m is not None:
+ log.debug("path matches {0!r}".format(regex.pattern))
+ if regex is usb_vid_pid:
+ match = [int(s.strip(':'), 16) for s in m.groups() if s]
+ match = dict(zip(['vid', 'pid'], match))
+ if regex is usb_bus_dev:
+ match = [int(s.strip(':'), 10) for s in m.groups() if s]
+ match = dict(zip(['bus', 'adr'], match))
+ if regex is usb_or_none:
+ match = dict()
+ break
+ else:
+ return None
+
+ with libusb.USBContext() as context:
+ devices = context.getDeviceList(skip_on_error=True)
+ vid, pid = match.get('vid'), match.get('pid')
+ bus, dev = match.get('bus'), match.get('adr')
+ if vid is not None:
+ devices = [d for d in devices if d.getVendorID() == vid]
+ if pid is not None:
+ devices = [d for d in devices if d.getProductID() == pid]
+ if bus is not None:
+ devices = [d for d in devices if d.getBusNumber() == bus]
+ if dev is not None:
+ devices = [d for d in devices if d.getDeviceAddress() == dev]
+ return [(d.getVendorID(), d.getProductID(), d.getBusNumber(),
+ d.getDeviceAddress()) for d in devices]
+
+ def __init__(self, usb_bus, dev_adr):
+ self.context = libusb.USBContext()
+ self.open(usb_bus, dev_adr)
+
+ def __del__(self):
+ self.close()
+ if self.context: # pragma: no branch
+ self.context.exit()
+
+ def open(self, usb_bus, dev_adr):
+ self.usb_dev = None
+ self.usb_out = None
+ self.usb_inp = None
+
+ for dev in self.context.getDeviceList(skip_on_error=True):
+ if ((dev.getBusNumber() == usb_bus and
+ dev.getDeviceAddress() == dev_adr)):
+ break
+ else:
+ log.error("no device {0} on bus {1}".format(dev_adr, usb_bus))
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ try:
+ first_setting = next(dev.iterSettings())
+ except StopIteration:
+ log.error("no usb configuration settings, please replug device")
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ def transfer_type(x):
+ return x & libusb.TRANSFER_TYPE_MASK
+
+ def endpoint_dir(x):
+ return x & libusb.ENDPOINT_DIR_MASK
+
+ for endpoint in first_setting.iterEndpoints():
+ ep_addr = endpoint.getAddress()
+ ep_attr = endpoint.getAttributes()
+ if transfer_type(ep_attr) == libusb.TRANSFER_TYPE_BULK:
+ if endpoint_dir(ep_addr) == libusb.ENDPOINT_IN:
+ if not self.usb_inp:
+ self.usb_inp = endpoint
+ if endpoint_dir(ep_addr) == libusb.ENDPOINT_OUT:
+ if not self.usb_out:
+ self.usb_out = endpoint
+
+ if not (self.usb_inp and self.usb_out):
+ log.error("no bulk endpoints for read and write")
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ try:
+ # workaround the PN533's buggy USB implementation
+ self._manufacturer_name = dev.getManufacturer()
+ self._product_name = dev.getProduct()
+ except libusb.USBErrorIO:
+ self._manufacturer_name = None
+ self._product_name = None
+
+ try:
+ self.usb_dev = dev.open()
+ self.usb_dev.claimInterface(0)
+ except libusb.USBErrorAccess:
+ raise IOError(errno.EACCES, os.strerror(errno.EACCES))
+ except libusb.USBErrorBusy:
+ raise IOError(errno.EBUSY, os.strerror(errno.EBUSY))
+ except libusb.USBErrorNoDevice:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+
+ def close(self):
+ if self.usb_dev:
+ self.usb_dev.close()
+ self.usb_dev = None
+ self.usb_out = None
+ self.usb_inp = None
+
+ @property
+ def manufacturer_name(self):
+ return self._manufacturer_name
+
+ @property
+ def product_name(self):
+ return self._product_name
+
+ def read(self, timeout=0):
+ if self.usb_inp is not None:
+ try:
+ ep_addr = self.usb_inp.getAddress()
+ frame = self.usb_dev.bulkRead(ep_addr, 300, timeout)
+ except libusb.USBErrorTimeout:
+ raise IOError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT))
+ except libusb.USBErrorNoDevice:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+ except libusb.USBError as error:
+ log.error("%r", error)
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+
+ if len(frame) == 0:
+ log.error("bulk read returned zero data")
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
+
+ frame = bytearray(frame)
+ log.log(logging.DEBUG-1, "<<< %s", hexlify(frame).decode())
+ return frame
+
+ def write(self, frame, timeout=0):
+ if self.usb_out is not None:
+ log.log(logging.DEBUG-1, ">>> %s", hexlify(frame).decode())
+ try:
+ ep_addr = self.usb_out.getAddress()
+ self.usb_dev.bulkWrite(ep_addr, bytes(frame), timeout)
+ if len(frame) % self.usb_out.getMaxPacketSize() == 0:
+ self.usb_dev.bulkWrite(ep_addr, b'', timeout)
+ except libusb.USBErrorTimeout:
+ raise IOError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT))
+ except libusb.USBErrorNoDevice:
+ raise IOError(errno.ENODEV, os.strerror(errno.ENODEV))
+ except libusb.USBError as error:
+ log.error("%r", error)
+ raise IOError(errno.EIO, os.strerror(errno.EIO))
diff --git a/src/lib/nfc/clf/udp.py b/src/lib/nfc/clf/udp.py
new file mode 100644
index 0000000..dbb629b
--- /dev/null
+++ b/src/lib/nfc/clf/udp.py
@@ -0,0 +1,577 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2012, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""Driver module for simulated contactless communication over
+UDP/IP. It can be activated with the device path ``udp::``
+where the optional *host* may be the IP address or name of the node
+where the targeted communication partner is listening on *port*. The
+default values for *host* and *port* are ``localhost:54321``.
+
+The driver implements almost all communication modes, with the current
+exception of active communication mode data exchange protocol.
+
+========== ======= ============
+function support remarks
+========== ======= ============
+sense_tta yes
+sense_ttb yes
+sense_ttf yes
+sense_dep no
+listen_tta yes
+listen_ttb yes
+listen_ttf yes
+listen_dep yes
+========== ======= ============
+
+"""
+import nfc.clf
+
+import time
+import errno
+import socket
+import select
+import operator
+from functools import reduce
+from binascii import hexlify, unhexlify
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Device(nfc.clf.device.Device):
+ def __init__(self, host, port):
+ host = socket.gethostbyname(host)
+ host, port = socket.getnameinfo((host, port), socket.NI_NUMERICHOST)
+ self.addr = (host, int(port))
+ self._path = "%s:%s" % (host, port)
+ self.socket = None
+ self._create_socket()
+
+ def close(self):
+ self.mute()
+
+ def mute(self):
+ if self.socket:
+ # send RFOFF when socket port != listen port
+ if self.socket.getsockname()[1] != self.addr[1] and self.rcvd_data:
+ self._send_data("RFOFF", b"", self.addr)
+ self.socket.close()
+ self.socket = None
+
+ def sense_tta(self, target):
+ self._create_socket()
+
+ log.debug("sense_tta for %s on %s:%d", target, *self.addr)
+
+ if target.brty not in ("106A", "212A", "424A"):
+ message = "unsupported bitrate {0}".format(target.brty)
+ raise nfc.clf.UnsupportedTargetError(message)
+
+ sens_req = (target.sens_req if target.sens_req else
+ bytearray.fromhex("26"))
+
+ log.debug("send SENS_REQ %s", hexlify(sens_req).decode())
+ try:
+ self._send_data(target.brty, sens_req, self.addr)
+ brty, sens_res, addr = self._recv_data(1.0, target.brty)
+ except nfc.clf.TimeoutError:
+ return None
+
+ log.debug("rcvd SENS_RES %s", hexlify(sens_res).decode())
+
+ if sens_res[0] & 0x1F == 0:
+ log.debug("type 1 tag target found")
+ target = nfc.clf.RemoteTarget(target.brty, _addr=addr)
+ target.sens_res = sens_res
+ if sens_res[1] & 0x0F == 0b1100:
+ rid_cmd = bytearray.fromhex("78 0000 00000000")
+ log.debug("send RID_CMD %s", hexlify(rid_cmd).decode())
+ try:
+ self._send_data(brty, rid_cmd, self.addr)
+ brty, rid_res, addr = self._recv_data(1.0, brty)
+ target.rid_res = rid_res
+ except nfc.clf.CommunicationError as error:
+ log.debug(error)
+ return None
+ return target
+
+ # other than type 1 tag
+ try:
+ if target.sel_req:
+ uid = target.sel_req
+ if len(uid) > 4:
+ uid = b"\x88" + uid
+ if len(uid) > 8:
+ uid = uid[0:4] + b"\x88" + uid[4:]
+ for i, sel_cmd in zip(range(0, len(uid), 4), b"\x93\x95\x97"):
+ sel_req = bytearray([sel_cmd, 0x70]) + uid[i:i+4]
+ sel_req.append(reduce(operator.xor, sel_req[2:6])) # BCC
+ log.debug("send SEL_REQ {}".format(
+ hexlify(sel_req).decode()))
+ self._send_data(brty, sel_req, addr)
+ brty, sel_res, addr = self._recv_data(0.5, brty)
+ log.debug("rcvd SEL_RES {}".format(
+ hexlify(sel_res).decode()))
+ uid = target.sel_req
+ else:
+ uid = bytearray()
+ for sel_cmd in b"\x93\x95\x97":
+ sdd_req = bytearray([sel_cmd, 0x20])
+ log.debug("send SDD_REQ {}".format(
+ hexlify(sdd_req).decode()))
+ self._send_data(brty, sdd_req, addr)
+ brty, sdd_res, addr = self._recv_data(0.5, brty)
+ log.debug("rcvd SDD_RES {}".format(
+ hexlify(sdd_res).decode()))
+ sel_req = bytearray([sel_cmd, 0x70]) + sdd_res
+ log.debug("send SEL_REQ {}".format(
+ hexlify(sel_req).decode()))
+ self._send_data(brty, sel_req, addr)
+ brty, sel_res, addr = self._recv_data(0.5, brty)
+ log.debug("rcvd SEL_RES {}".format(
+ hexlify(sel_res).decode()))
+ if sel_res[0] & 0b00000100:
+ uid = uid + sdd_res[1:4]
+ else:
+ uid = uid + sdd_res[0:4]
+ break
+ if sel_res[0] & 0b00000100 == 0:
+ target = nfc.clf.RemoteTarget(target.brty, _addr=addr)
+ target.sens_res = sens_res
+ target.sel_res = sel_res
+ target.sdd_res = uid
+ return target
+ except nfc.clf.CommunicationError as error:
+ log.debug(error)
+
+ def sense_ttb(self, target):
+ self._create_socket()
+
+ if target.brty not in ("106B", "212B", "424B"):
+ message = "unsupported bitrate {0}".format(target.brty)
+ raise nfc.clf.UnsupportedTargetError(message)
+
+ sensb_req = (target.sensb_req if target.sensb_req else
+ bytearray.fromhex("050010"))
+
+ log.debug("send SENSB_REQ %s", hexlify(sensb_req).decode())
+ try:
+ self._send_data(target.brty, sensb_req, self.addr)
+ brty, sensb_res, addr = self._recv_data(1.0, target.brty)
+ except nfc.clf.CommunicationError:
+ return None
+
+ if len(sensb_res) >= 12 and sensb_res[0] == 0x50:
+ log.debug("rcvd SENSB_RES %s", hexlify(sensb_res).decode())
+ return nfc.clf.RemoteTarget(brty, sensb_res=sensb_res, _addr=addr)
+
+ def sense_ttf(self, target):
+ self._create_socket()
+
+ log.debug("sense_ttf for %s on %s:%d", target, *self.addr)
+
+ if target.brty not in ("212F", "424F"):
+ message = "unsupported bitrate {0}".format(target.brty)
+ raise nfc.clf.UnsupportedTargetError(message)
+
+ if not target.sensf_req:
+ sensf_req = bytearray.fromhex("0600FFFF0100")
+ else:
+ sensf_req = bytearray([len(target.sensf_req)+1]) + target.sensf_req
+
+ log.debug("send SENSF_REQ {}".format(
+ hexlify(memoryview(sensf_req)[1:]).decode()))
+ try:
+ self._send_data(target.brty, sensf_req, self.addr)
+ brty, data, addr = self._recv_data(1.0, target.brty)
+ except nfc.clf.CommunicationError:
+ return None
+
+ if len(data) >= 18 and data[0] == len(data) and data[1] == 1:
+ log.debug("rcvd SENSF_RES %s", hexlify(data[1:]).decode())
+ return nfc.clf.RemoteTarget(brty, sensf_res=data[1:], _addr=addr)
+
+ def sense_dep(self, target):
+ info = "{device} does not support sense for active DEP Target"
+ raise nfc.clf.UnsupportedTargetError(info.format(device=self))
+
+ def listen_tta(self, target, timeout):
+ self._create_socket()
+
+ log.debug("listen_tta for %.3f seconds on %s:%d", timeout, *self.addr)
+
+ time_to_return = time.time() + timeout
+ if not self._bind_socket(time_to_return):
+ log.debug("failed to bind socket")
+ return None
+
+ log.debug("wait for data on socket %s:%d", *self.socket.getsockname())
+ return self._listen_tta(target, time_to_return)
+
+ def _listen_tta(self, target, time_to_return, init=None):
+ sdd_res = bytearray(target.sdd_res)
+ if len(sdd_res) > 4:
+ sdd_res.insert(0, 0x88)
+ if len(sdd_res) > 8:
+ sdd_res.insert(4, 0x88)
+ sdd_res.insert(4, reduce(operator.xor, sdd_res[0:4]))
+ if len(sdd_res) > 5:
+ sdd_res.insert(9, reduce(operator.xor, sdd_res[5:9]))
+ if len(sdd_res) > 10:
+ sdd_res.insert(14, reduce(operator.xor, sdd_res[10:14]))
+ sel_res = bytearray([target.sel_res[0] & 0b11111011])
+
+ while time.time() < time_to_return:
+ if init is None:
+ wait = max(0.5, time_to_return - time.time())
+ try:
+ brty, data, addr = self._recv_data(wait, target.brty)
+ except nfc.clf.TimeoutError:
+ return None
+ except nfc.clf.CommunicationError:
+ continue
+ else:
+ (brty, data, addr), init = init, None
+ if data == b"\x26":
+ log.debug("rcvd SENS_REQ %s", hexlify(data).decode())
+ sens_res = target.sens_res
+ log.debug("send SENS_RES %s", hexlify(sens_res).decode())
+ self._send_data(brty, sens_res, addr)
+ elif data == b"\x93\x20":
+ log.debug("rcvd SDD_REQ CL1 %s", hexlify(data).decode())
+ log.debug("send SDD_RES CL1 %s",
+ hexlify(sdd_res[0:5]).decode())
+ self._send_data(brty, sdd_res[0:5], addr)
+ elif data == b"\x95\x20" and len(sdd_res) > 5:
+ log.debug("rcvd SDD_REQ CL2 %s", hexlify(data).decode())
+ log.debug("send SDD_RES CL2 %s",
+ hexlify(sdd_res[5:10]).decode())
+ self._send_data(brty, sdd_res[5:10], addr)
+ elif data == b"\x97\x20" and len(sdd_res) > 10:
+ log.debug("rcvd SDD_REQ CL3 %s", hexlify(data).decode())
+ log.debug("send SDD_RES CL3 %s",
+ hexlify(sdd_res[10:15]).decode())
+ self._send_data(brty, sdd_res[10:15], addr)
+ elif data == b"\x93\x70" + sdd_res[0:5]:
+ log.debug("rcvd SEL_REQ Cl1 %s", hexlify(data).decode())
+ sel_res[0] = (sel_res[0] & 0xFB) | (len(sdd_res) > 5) << 2
+ log.debug("send SEL_RES %s", hexlify(sel_res).decode())
+ self._send_data(brty, sel_res, addr)
+ elif data == b"\x95\x70" + sdd_res[5:10]:
+ log.debug("rcvd SEL_REQ CL2 %s", hexlify(data).decode())
+ sel_res[0] = (sel_res[0] & 0xFB) | (len(sdd_res) > 10) << 2
+ log.debug("send SEL_RES %s", hexlify(sel_res).decode())
+ self._send_data(brty, sel_res, addr)
+ elif data == b"\x95\x70" + sdd_res[10:15]:
+ log.debug("rcvd SEL_REQ CL3 %s", hexlify(data).decode())
+ sel_res[0] = (sel_res[0] & 0xFB) | (len(sdd_res) > 15) << 2
+ log.debug("send SEL_RES %s", hexlify(sel_res).decode())
+ self._send_data(brty, sel_res, addr)
+ elif sel_res[0] & 0b00000100 == 0:
+ target = nfc.clf.LocalTarget(
+ brty, _addr=addr, sens_res=target.sens_res,
+ sdd_res=target.sdd_res, sel_res=target.sel_res)
+ if ((data[0] == 0xF0 and len(data) >= 18 and
+ data[1] == len(data)-1 and data[2:4] == b"\xD4\x00")):
+ target.atr_req = data[2:]
+ elif data[0] == 0xE0:
+ target.tt4_cmd = data[:]
+ else:
+ target.tt2_cmd = data[:]
+ return target
+
+ def listen_ttb(self, target, timeout):
+ self._create_socket()
+
+ log.debug("listen_ttb for %.3f seconds on %s:%d", timeout, *self.addr)
+
+ time_to_return = time.time() + timeout
+ if not self._bind_socket(time_to_return):
+ log.debug("failed to bind socket")
+ return None
+
+ assert target.sensb_res and len(target.sensb_res) >= 12
+ log.debug("wait for data on socket %s:%d", *self.socket.getsockname())
+
+ while time.time() < time_to_return:
+ wait = max(0.5, time_to_return - time.time())
+ try:
+ brty, data, addr = self._recv_data(wait, target.brty)
+ except nfc.clf.TimeoutError:
+ return None
+ except nfc.clf.CommunicationError:
+ continue
+ if data and len(data) == 3 and data.startswith(b'\x05'):
+ req = "ALLB_REQ" if data[1] & 0x08 else "SENSB_REQ"
+ sensb_req = data
+ log.debug("rcvd %s %s", req, hexlify(sensb_req).decode())
+ log.debug("send SENSB_RES %s",
+ hexlify(target.sensb_res).decode())
+ self._send_data(brty, target.sensb_res, addr)
+ brty, data, addr = self._recv_data(wait, target.brty)
+ return nfc.clf.LocalTarget(brty, sensb_req=sensb_req,
+ sensb_res=target.sensb_res,
+ tt4_cmd=data, _addr=addr)
+
+ def listen_ttf(self, target, timeout):
+ self._create_socket()
+
+ log.debug("listen_ttf for %.3f seconds on %s:%d", timeout, *self.addr)
+
+ time_to_return = time.time() + timeout
+ if not self._bind_socket(time_to_return):
+ log.debug("failed to bind socket")
+ return None
+
+ log.debug("wait for data on socket %s:%d", *self.socket.getsockname())
+ return self._listen_ttf(target, time_to_return)
+
+ def _listen_ttf(self, target, time_to_return, init=None):
+ sensf_req = sensf_res = None
+ while time.time() < time_to_return:
+ if init is None:
+ wait = max(0.5, time_to_return - time.time())
+ try:
+ brty, data, addr = self._recv_data(wait, target.brty)
+ except nfc.clf.TimeoutError:
+ return None
+ except nfc.clf.CommunicationError:
+ continue
+ else:
+ (brty, data, addr), init = init, None
+ if data and len(data) == data[0]:
+ if data.startswith(b"\x06\x00"):
+ (sensf_req, sensf_res) = (data[1:], target.sensf_res[:])
+ if (((sensf_req[1] == 255 or
+ sensf_req[1] == sensf_res[17]) and
+ (sensf_req[2] == 255 or
+ sensf_req[2] == sensf_res[18]))):
+ data = sensf_res[0:17]
+ if sensf_req[3] == 1:
+ data += sensf_res[17:19]
+ if sensf_req[3] == 2:
+ data += bytearray(
+ [0x00, 1 << (target.brty == "424F")])
+ data = bytearray([len(data)+1]) + data
+ self._send_data(brty, data, addr)
+ else:
+ sensf_req = sensf_res = None
+ elif sensf_req and sensf_res:
+ if data[2:10] == target.sensf_res[1:9]:
+ target = nfc.clf.LocalTarget(brty, _addr=addr)
+ target.sensf_req = sensf_req
+ target.sensf_res = sensf_res
+ target.tt3_cmd = data[1:]
+ return target
+ if data[1:11] == b'\xD4\x00' + target.sensf_res[1:9]:
+ target = nfc.clf.LocalTarget(brty, _addr=addr)
+ target.sensf_req = sensf_req
+ target.sensf_res = sensf_res
+ target.atr_req = data[1:]
+ return target
+
+ def listen_dep(self, target, timeout):
+ self._create_socket()
+
+ log.debug("listen_dep for %.3f seconds on %s:%d", timeout, *self.addr)
+ assert target.sensf_res is not None
+ assert target.sens_res is not None
+ assert target.sdd_res is not None
+ assert target.sel_res is not None
+ assert target.atr_res is not None
+ assert len(target.sensf_res) == 19
+ assert len(target.sens_res) == 2
+ assert len(target.sdd_res) == 4
+ assert len(target.sel_res) == 1
+ assert len(target.atr_res) >= 17 and len(target.atr_res) <= 64
+
+ time_to_return = time.time() + timeout
+ if not self._bind_socket(time_to_return):
+ log.debug("failed to bind socket")
+ return None
+
+ log.debug("wait for data on socket %s:%d", *self.socket.getsockname())
+ atr_res = bytearray(target.atr_res)
+
+ while time.time() < time_to_return:
+ wait = max(0, time_to_return - time.time())
+ try:
+ result = self._recv_data(wait, '106A', '212F', '424F')
+ brty, data, addr = result
+ except nfc.clf.CommunicationError:
+ return None
+
+ target.brty = brty
+ if brty == '106A':
+ if data == b"\x26":
+ init = (brty, data, addr)
+ target = self._listen_tta(target, time_to_return, init)
+ elif (len(data) >= 18 and data[1] == len(data)-1 and
+ data[0] == 0xF0 and data[2:4] == b'\xD4\x00'):
+ target = nfc.clf.LocalTarget(
+ brty, atr_res=target.atr_res, atr_req=data[2:])
+ elif brty in ('212F', '424F') and data[0] == len(data):
+ if data.startswith(b'\x06\x00'):
+ init = (brty, data, addr)
+ target = self._listen_ttf(target, time_to_return, init)
+ elif len(data) >= 17 and data[1:3] == b'\xD4\x00':
+ target = nfc.clf.LocalTarget(
+ brty, atr_res=target.atr_res, atr_req=data[1:])
+
+ if target and target.atr_req:
+ target.atr_res = atr_res
+ log.debug("rcvd ATR_REQ %s", hexlify(target.atr_req).decode())
+ log.debug("send ATR_RES %s", hexlify(target.atr_res).decode())
+ data = bytearray([len(atr_res) + 1]) + atr_res
+ if brty == '106A':
+ data.insert(0, 0xF0)
+ self._send_data(brty, data, addr)
+ brty, data, addr = self._recv_data(wait, brty)
+ try:
+ if brty == '106A':
+ assert data.pop(0) == 0xF0
+ assert len(data) == data.pop(0)
+ except AssertionError:
+ return None
+ if data.startswith(b'\xD4\x04'):
+ target.psl_req = data[:]
+ target.psl_res = b'\xD5\x05' + target.psl_req[2:3]
+ log.debug("rcvd PSL_REQ %s",
+ hexlify(target.psl_req).decode())
+ log.debug("send PSL_RES %s",
+ hexlify(target.psl_res).decode())
+ data = bytearray([len(target.psl_res) + 1]) \
+ + target.psl_res
+ if brty == '106A':
+ data.insert(0, 0xF0)
+ self._send_data(brty, data, addr)
+ brty = ('106A', '212F', '424F')[target.psl_req[3] >> 3 & 7]
+ target.brty, data, addr = self._recv_data(wait, brty)
+ try:
+ if brty == '106A':
+ assert data.pop(0) == 0xF0
+ assert len(data) == data.pop(0)
+ except AssertionError:
+ return None
+ if data.startswith(b'\xD4\x08'):
+ log.debug("rcvd DSL_REQ %s", hexlify(data).decode())
+ data = b'\xD5\x09' + data[2:3]
+ log.debug("send DSL_RES %s", hexlify(data).decode())
+ data = bytearray([len(data) + 1]) + data
+ if brty == '106A':
+ data.insert(0, 0xF0)
+ self._send_data(brty, data, addr)
+ return None
+ if data.startswith(b'\xD4\x0A'):
+ log.debug("rcvd RLS_REQ %s", hexlify(data).decode())
+ data = b'\xD5\x0B' + data[2:3]
+ log.debug("send RLS_RES %s", hexlify(data).decode())
+ data = bytearray([len(data) + 1]) + data
+ if brty == '106A':
+ data.insert(0, 0xF0)
+ self._send_data(brty, data, addr)
+ return None
+ if data.startswith(b'\xD4\x06'):
+ target.dep_req = data[:]
+ return target
+ return None
+
+ def send_cmd_recv_rsp(self, target, data, timeout):
+ # send data, data should normally not be None for the Initiator
+ if data is not None:
+ self._send_data(target.brty, data, target._addr)
+
+ # receive response data unless the timeout is zero
+ if timeout > 0:
+ brty, data, addr = self._recv_data(timeout, target.brty)
+ return data
+
+ def send_rsp_recv_cmd(self, target, data, timeout):
+ # send data, data may be none as target keeps silence on error
+ if data is not None:
+ self._send_data(target.brty, data, target._addr)
+
+ # recv response data unless the timeout is zero
+ if timeout is None or timeout > 0:
+ brty, data, addr = self._recv_data(timeout, target.brty)
+ return data
+
+ def get_max_send_data_size(self, target):
+ return 290
+
+ def get_max_recv_data_size(self, target):
+ return 290
+
+ def _create_socket(self):
+ if self.socket is None:
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ self.sent_data = self.rcvd_data = 0
+
+ def _bind_socket(self, time_to_return):
+ addr = ('0.0.0.0', self.addr[1])
+ while time.time() < time_to_return:
+ log.debug("trying to bind socket to %s:%d", *addr)
+ try:
+ self.socket.bind(addr)
+ return True
+ except socket.error as error:
+ log.debug("bind failed with %s", error)
+ if error.errno == errno.EADDRINUSE:
+ return False
+ else:
+ raise error
+
+ def _send_data(self, brty, data, addr):
+ data = (b"%s %s" % (brty.encode('latin'), hexlify(data))).strip()
+ log.log(logging.DEBUG-1, ">>> %s to %s:%s", data.decode(), *addr)
+ ret = self.socket.sendto(data, addr)
+ if ret != len(data):
+ raise nfc.clf.TransmissionError("failed to send data")
+ self.sent_data += len(data)
+
+ def _recv_data(self, timeout, *brty_list):
+ time_to_return = None if timeout is None else (time.time() + timeout)
+ while timeout is None or time.time() < time_to_return:
+ wait = None if timeout is None else (time_to_return - time.time())
+ if len(select.select([self.socket], [], [], wait)[0]) == 1:
+ data, addr = self.socket.recvfrom(1024)
+ log.log(logging.DEBUG-1, "<<< %s from %s:%d", data, *addr)
+ if data.startswith(b"RFOFF"):
+ raise nfc.clf.BrokenLinkError("RFOFF")
+ try:
+ brty, data = data.split()
+ except ValueError:
+ raise nfc.clf.TransmissionError("no data")
+ brty = brty.decode("ascii")
+ data = bytearray(unhexlify(data))
+ self.rcvd_data += len(data)
+ if brty in brty_list:
+ return brty, data, addr
+ raise nfc.clf.TimeoutError("no data received")
+
+
+def init(host, port):
+ import platform
+ device = Device(host, port)
+ device._vendor_name = platform.uname()[0]
+ device._device_name = "IP-Stack"
+ device._chipset_name = "UDP"
+ return device
diff --git a/src/lib/nfc/dep.py b/src/lib/nfc/dep.py
new file mode 100644
index 0000000..6d93bde
--- /dev/null
+++ b/src/lib/nfc/dep.py
@@ -0,0 +1,895 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import src.lib.nfc.clf
+
+import os
+import time
+import collections
+import struct
+from binascii import hexlify
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class DataExchangeProtocol(object):
+ class Counter(object):
+ def __init__(self):
+ self.sent = collections.defaultdict(int)
+ self.rcvd = collections.defaultdict(int)
+
+ @property
+ def sent_count(self):
+ return sum(self.sent.values())
+
+ @property
+ def rcvd_count(self):
+ return sum(self.rcvd.values())
+
+ def __str__(self):
+ s = "sent/rcvd {0}/{1}".format(self.sent_count, self.rcvd_count)
+ for name in sorted(set(list(self.sent.keys())
+ + list(self.rcvd.keys()))):
+ s += " {name} {sent}/{rcvd}".format(
+ name=name, sent=self.sent[name], rcvd=self.rcvd[name])
+ return s
+
+ def __init__(self, clf):
+ self.pcnt = DataExchangeProtocol.Counter()
+ self.clf = clf
+ self.gbi = b""
+ self.gbt = b""
+
+ @property
+ def general_bytes(self):
+ """The general bytes received with the ATR exchange"""
+ pass
+
+ @property
+ def role(self):
+ """Role in DEP communication, either 'Target' or 'Initiator'"""
+ pass
+
+
+class Initiator(DataExchangeProtocol):
+ ROLE = "Initiator"
+
+ def __init__(self, clf):
+ DataExchangeProtocol.__init__(self, clf)
+ self.target = None
+ self.miu = None # maximum information unit size
+ self.did = None # dep device identifier
+ self.nad = None # dep node address
+ self.gbt = None # general bytes from target
+ self.pni = None # dep packet number information
+ self.rwt = None # target response waiting time
+ self._acm = None # active communication mode flag
+
+ @property
+ def role(self):
+ return "Initiator"
+
+ @property
+ def general_bytes(self):
+ return self.gbt
+
+ @property
+ def acm(self):
+ return bool(self._acm)
+
+ def __str__(self):
+ msg = "NFC-DEP Initiator {brty} {mode} mode MIU={miu} RWT={rwt:.6f}"
+ return msg.format(brty=self.target.brty, miu=self.miu, rwt=self.rwt,
+ mode=("passive", "active")[self.acm])
+
+ def activate(self, target=None, **options):
+ """Activate DEP communication with a target."""
+ log.debug("initiator options: {0}".format(options))
+
+ self.did = options.get('did', None)
+ self.nad = options.get('nad', None)
+ self.gbi = options.get('gbi', b'')[0:48]
+ self.brs = min(max(0, options.get('brs', 2)), 2)
+ self.lri = min(max(0, options.get('lri', 3)), 3)
+ if self._acm is None or 'acm' in options:
+ self._acm = bool(options.get('acm', True))
+
+ assert self.did is None or 0 <= self.did <= 255
+ assert self.nad is None or 0 <= self.nad <= 255
+
+ ppi = (self.lri << 4) | (bool(self.gbi) << 1) | int(bool(self.nad))
+ did = 0 if self.did is None else self.did
+ atr_req = ATR_REQ(os.urandom(10), did, 0, 0, ppi, self.gbi)
+ psl_req = PSL_REQ(did, (0, 9, 18)[self.brs], self.lri)
+ atr_res = psl_res = None
+ self.target = target
+
+ if self.target is None and self.acm is True:
+ log.debug("searching active communication mode target at 106A")
+ tg = nfc.clf.RemoteTarget("106A", atr_req=atr_req.encode())
+ try:
+ self.target = self.clf.sense(tg, iterations=2, interval=0.1)
+ except nfc.clf.UnsupportedTargetError:
+ self._acm = False
+ except nfc.clf.CommunicationError:
+ pass
+ else:
+ if self.target:
+ atr_res = ATR_RES.decode(self.target.atr_res)
+ else:
+ self._acm = None
+
+ if self.target is None:
+ log.debug("searching passive communication mode target at 106A")
+ target = nfc.clf.RemoteTarget("106A")
+ target = self.clf.sense(target, iterations=2, interval=0.1)
+ if target and target.sel_res and bool(target.sel_res[0] & 0x40):
+ self.target = target
+
+ if self.target is None and self.brs > 0:
+ log.debug("searching passive communication mode target at 212F")
+ target = nfc.clf.RemoteTarget("212F", sensf_req=b'\0\xFF\xFF\0\0')
+ target = self.clf.sense(target, iterations=2, interval=0.1)
+ if target and target.sensf_res.startswith(b'\1\1\xFE'):
+ atr_req.nfcid3 = target.sensf_res[1:9] + b'ST'
+ self.target = target
+
+ if self.target and self.target.atr_res is None:
+ try:
+ atr_res = self.send_req_recv_res(atr_req, 1.0)
+ except nfc.clf.CommunicationError:
+ pass
+ if atr_res is None:
+ log.debug("NFC-DEP Attribute Request failed")
+ return None
+
+ if self.target and atr_res:
+ if self.brs > ('106A', '212F', '424F').index(self.target.brty):
+ try:
+ psl_res = self.send_req_recv_res(psl_req, 0.1)
+ except nfc.clf.CommunicationError:
+ pass
+ if psl_res is None:
+ log.debug("NFC-DEP Parameter Selection failed")
+ return None
+ self.target.brty = ('212F', '424F')[self.brs-1]
+
+ self.rwt = (4096/13.56E6
+ * 2**(atr_res.wt if atr_res.wt < 15 else 14))
+ self.miu = (atr_res.lr-3 - int(self.did is not None)
+ - int(self.nad is not None))
+ self.gbt = atr_res.gb
+ self.pni = 0
+
+ log.info("running as " + str(self))
+ return self.gbt
+
+ def deactivate(self, release=True):
+ log.debug("deactivate {0}".format(self))
+ req = RLS_REQ(self.did) if release else DSL_REQ(self.did)
+ try:
+ res = self.send_req_recv_res(req, 0.1)
+ except nfc.clf.CommunicationError:
+ return
+ else:
+ if res.did != req.did:
+ log.error("target returned wrong DID in " + res.PDU_NAME)
+ finally:
+ log.debug("packets {0}".format(self.pcnt))
+
+ def exchange(self, send_data, timeout):
+ def INF(pni, data, more, did, nad):
+ pdu_type = (DEP_REQ.LastInformation, DEP_REQ.MoreInformation)[more]
+ pfb = DEP_REQ.PFB(pdu_type, nad is not None, did is not None, pni)
+ return DEP_REQ(pfb, did, nad, data)
+
+ def ACK(pni, did, nad):
+ pdu_type = DEP_REQ.PositiveAck
+ pfb = DEP_REQ.PFB(pdu_type, nad is not None, did is not None, pni)
+ return DEP_REQ(pfb, did, nad, data=None)
+
+ def RTOX(rtox, did, nad):
+ if not 0 < rtox < 60:
+ error = "NFC-DEP RTOX must be in range 1 to 59"
+ raise nfc.clf.ProtocolError(error)
+ pdu_type = DEP_REQ.TimeoutExtension
+ pfb = DEP_REQ.PFB(pdu_type, nad is not None, did is not None, 0)
+ return DEP_REQ(pfb, did, nad, data=bytearray([rtox]))
+
+ # log.debug("dep raw >> %s", hexlify(send_data).decode())
+ send_data = bytearray(send_data)
+
+ while send_data:
+ data = send_data[0:self.miu]
+ del send_data[0:self.miu]
+ req = INF(self.pni, data, bool(send_data), self.did, self.nad)
+ res = self.send_dep_req_recv_dep_res(req, self.rwt, timeout)
+ if res.pfb.fmt == DEP_RES.TimeoutExtension:
+ for i in range(3):
+ req = RTOX(res.data[0], self.did, self.nad)
+ rwt = res.data[0] * self.rwt
+ log.warning("target requested %.3f sec more time", rwt)
+ res = self.send_dep_req_recv_dep_res(req, rwt, timeout)
+ if res.pfb.fmt != DEP_RES.TimeoutExtension:
+ break
+ else:
+ log.error("too many timeout extension requests")
+ raise nfc.clf.TimeoutError("timeout extension")
+ if res.pfb.fmt == DEP_RES.PositiveAck:
+ if not send_data:
+ error = "unexpected or out-of-sequence NFC-DEP ACK PDU"
+ raise nfc.clf.ProtocolError(error)
+ if res.pfb.pni != self.pni:
+ raise nfc.clf.ProtocolError("wrong NFC-DEP packet number")
+ self.pni = (self.pni + 1) & 0x3
+
+ if ((res.pfb.fmt != DEP_RES.LastInformation and
+ res.pfb.fmt != DEP_RES.MoreInformation)):
+ error = "expected NFC-DEP INF PDU after sending"
+ raise nfc.clf.ProtocolError(error)
+
+ recv_data = res.data
+
+ while res.pfb.fmt == DEP_RES.MoreInformation:
+ req = ACK(self.pni, self.did, self.nad)
+ res = self.send_dep_req_recv_dep_res(req, self.rwt, timeout)
+ if res.pfb.fmt == DEP_RES.TimeoutExtension:
+ for i in range(3):
+ req = RTOX(res.data[0], self.did, self.nad)
+ rwt = res.data[0] * self.rwt
+ log.warning("target requested %.3f sec more time", rwt)
+ res = self.send_dep_req_recv_dep_res(req, rwt, timeout)
+ if res.pfb.fmt != DEP_RES.TimeoutExtension:
+ break
+ else:
+ log.error("too many timeout extension requests")
+ raise nfc.clf.TimeoutError("timeout extension")
+ if ((res.pfb.fmt != DEP_RES.LastInformation and
+ res.pfb.fmt != DEP_RES.MoreInformation)):
+ error = "NFC-DEP chaining not continued after ACK"
+ raise nfc.clf.ProtocolError(error)
+ if res.pfb.pni != self.pni:
+ raise nfc.clf.ProtocolError("wrong NFC-DEP packet number")
+ recv_data += res.data
+ self.pni = (self.pni + 1) & 0x3
+
+ # log.debug("dep raw << %s", hexlify(recv_data).decode())
+ return recv_data
+
+ def send_dep_req_recv_dep_res(self, req, rwt, timeout):
+ def NAK(pni, did, nad):
+ pdu_type = DEP_REQ.NegativeAck
+ pfb = DEP_REQ.PFB(
+ pdu_type, nad is not None, did is not None, self.pni)
+ return DEP_REQ(pfb, did, nad, data=None)
+
+ def ATN():
+ pdu_type = DEP_REQ.Attention
+ pfb = DEP_REQ.PFB(pdu_type, nad=False, did=False, pni=0)
+ return DEP_REQ(pfb, did=None, nad=None, data=None)
+
+ def request_attention(self, n_retry_atn, rwt, deadline):
+ req = ATN()
+ for i in range(n_retry_atn):
+ timeout = min(rwt, deadline - time.time())
+ if timeout <= 0:
+ raise nfc.clf.TimeoutError
+ try:
+ res = self.send_req_recv_res(req, timeout)
+ except nfc.clf.CommunicationError:
+ continue
+ if res.pfb.fmt == DEP_RES.TimeoutExtension:
+ error = "received NFC-DEP RTOX response to NACK or ATN"
+ raise nfc.clf.ProtocolError(error)
+ if res.pfb.fmt != DEP_RES.Attention:
+ error = "expected NFC-DEP Attention response"
+ raise nfc.clf.ProtocolError(error)
+ return
+ error = "unrecoverable NFC-DEP error in attention request"
+ raise nfc.clf.ProtocolError(error)
+
+ def request_retransmission(self, n_retry_nak, rwt, deadline):
+ req = NAK(self.pni, self.did, self.nad)
+ for i in range(n_retry_nak):
+ timeout = min(rwt, deadline - time.time())
+ if timeout <= 0:
+ raise nfc.clf.TimeoutError
+ try:
+ res = self.send_req_recv_res(req, timeout)
+ except nfc.clf.CommunicationError:
+ continue
+ if res.pfb.fmt == DEP_RES.TimeoutExtension:
+ error = "received NFC-DEP RTOX response to NACK or ATN"
+ raise nfc.clf.ProtocolError(error)
+ expected = (DEP_RES.LastInformation, DEP_RES.MoreInformation)
+ if res.pfb.fmt not in expected:
+ error = "unrecoverable NFC-DEP transmission error"
+ raise nfc.clf.ProtocolError(error)
+ return res
+ error = "unrecoverable NFC-DEP error in retransmission request"
+ raise nfc.clf.ProtocolError(error)
+
+ if rwt > timeout:
+ text = "response waiting time %.3f exceeds the timeout of %.3f sec"
+ log.warning(text, rwt, timeout)
+
+ deadline = time.time() + timeout
+ while True:
+ timeout = min(rwt, deadline - time.time())
+ if timeout <= 0:
+ raise nfc.clf.TimeoutError()
+ try:
+ res = self.send_req_recv_res(req, timeout)
+ break
+ except nfc.clf.TimeoutError:
+ request_attention(self, 2, rwt, deadline)
+ continue
+ except nfc.clf.TransmissionError:
+ res = request_retransmission(self, 2, rwt, deadline)
+ break
+
+ if res.pfb.fmt == DEP_RES.NegativeAck:
+ error = "received NFC-DEP NACK PDU from Target"
+ raise nfc.clf.ProtocolError(error)
+
+ return res
+
+ def send_req_recv_res(self, req, timeout):
+ log.debug(">> {0}".format(req))
+ pcnt_key = req.PDU_NAME[:3]
+ if isinstance(req, DEP_REQ):
+ pcnt_key += " " + req.pfb.FMT_NAME
+ self.pcnt.sent[pcnt_key] += 1
+
+ cmd = self.encode_frame(req)
+ rsp = self.clf.exchange(cmd, timeout)
+ res = self.decode_frame(rsp)
+ if res.PDU_NAME[0:3] != req.PDU_NAME[0:3]:
+ raise nfc.clf.ProtocolError("invalid response for " + req.PDU_NAME)
+
+ log.debug("<< {0}".format(res))
+ pcnt_key = res.PDU_NAME[:3]
+ if isinstance(res, DEP_RES):
+ pcnt_key += " " + res.pfb.FMT_NAME
+ self.pcnt.rcvd[pcnt_key] += 1
+ return res
+
+ def encode_frame(self, packet):
+ frame = packet.encode()
+ frame = struct.pack("B", len(frame) + 1) + frame
+ if self.target.brty == '106A':
+ frame = b'\xF0' + frame
+ return bytearray(frame)
+
+ def decode_frame(self, frame):
+ if self.target.brty == '106A' and frame.pop(0) != 0xF0:
+ error = "first NFC-DEP frame byte must be F0h for 106A"
+ raise nfc.clf.ProtocolError(error)
+ if len(frame) != frame.pop(0):
+ error = "NFC-DEP frame length byte must be data length + 1"
+ raise nfc.clf.ProtocolError(error)
+ if len(frame) < 2:
+ error = "NFC-DEP frame length byte must be from 3 to 255"
+ raise nfc.clf.TransmissionError(error)
+ if frame[0] != 0xD5 or frame[1] not in (1, 5, 7, 9, 11):
+ raise nfc.clf.ProtocolError("invalid NFC-DEP response code")
+ res_name = {1: 'ATR', 5: 'PSL', 7: 'DEP', 9: 'DSL', 11: 'RLS'}
+ return eval(res_name[frame[1]] + "_RES").decode(frame)
+
+
+class Target(DataExchangeProtocol):
+ def __init__(self, clf):
+ DataExchangeProtocol.__init__(self, clf)
+ self.miu = None # maximum information unit size
+ self.did = None # dep device identifier
+ self.nad = None # dep node address
+ self.gbi = None # general bytes from initiator
+ self.pni = None # dep packet number information
+ self.rwt = None # target response waiting time
+
+ @property
+ def role(self):
+ return "Target"
+
+ @property
+ def general_bytes(self):
+ return self.gbi
+
+ def __str__(self):
+ msg = "NFC-DEP Target {brty} {mode} mode MIU={miu} RWT={rwt:.6f}"
+ return msg.format(brty=self.target.brty, miu=self.miu, rwt=self.rwt,
+ mode=("passive", "active")[self.acm])
+
+ def activate(self, timeout=None, **options):
+ """Activate DEP communication as a target."""
+
+ if timeout is None:
+ timeout = 1.0
+ gbt = options.get('gbt', b'')[0:47]
+ lrt = min(max(0, options.get('lrt', 3)), 3)
+ rwt = min(max(0, options.get('rwt', 8)), 14)
+
+ pp = (lrt << 4) | (bool(gbt) << 1) | int(bool(self.nad))
+ nfcid3t = bytearray.fromhex("01FE") + os.urandom(6) + b"ST"
+ atr_res = ATR_RES(nfcid3t, 0, 0, 0, rwt, pp, gbt)
+ atr_res = atr_res.encode()
+
+ target = nfc.clf.LocalTarget(atr_res=atr_res)
+ target.sens_res = bytearray.fromhex("0101")
+ target.sdd_res = bytearray.fromhex("08") + os.urandom(3)
+ target.sel_res = bytearray.fromhex("40")
+ target.sensf_res = bytearray.fromhex("01") + nfcid3t[0:8]
+ target.sensf_res += bytearray.fromhex("00000000 00000000 FFFF")
+
+ target = self.clf.listen(target, timeout)
+
+ if target and target.atr_req and target.dep_req:
+ log.debug("activated as " + str(target))
+
+ atr_req = ATR_REQ.decode(target.atr_req)
+ self.lrt = lrt
+ self.gbt = gbt
+ self.gbi = atr_req.gb
+ self.miu = atr_req.lr - 3
+ self.rwt = 4096/13.56E6 * pow(2, rwt)
+ self.did = atr_req.did if atr_req.did > 0 else None
+ self.acm = not (target.sens_res or target.sensf_res)
+ self.cmd = bytearray(
+ struct.pack("B", len(target.dep_req)+1) + target.dep_req)
+ if target.brty == "106A":
+ self.cmd = bytearray(b"\xF0" + self.cmd)
+ self.target = target
+
+ self.pcnt.rcvd["ATR"] += 1
+ self.pcnt.sent["ATR"] += 1
+ log.info("running as " + str(self))
+
+ return self.gbi
+
+ def deactivate(self, data=bytearray()):
+ try:
+ log.debug("deactivate {0}".format(self))
+ self._deactivate(data)
+ finally:
+ log.debug("packets {0}".format(self.pcnt))
+
+ def _deactivate(self, data):
+ def INF(pni, data, did, nad):
+ pdu_type = DEP_RES.LastInformation
+ pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, pni)
+ return DEP_RES(pfb, did, nad, data)
+
+ def ATN(did, nad):
+ pdu_type = DEP_RES.Attention
+ pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, 0)
+ return DEP_RES(pfb, did, nad, data=None)
+
+ res = None
+ deadline = time.time() + 1.0
+ while time.time() < deadline: # pragma: no branch
+ try:
+ req = self.send_res_recv_req(res, deadline)
+ except nfc.clf.CommunicationError:
+ return
+ if req is None:
+ return
+ if req.did == self.did:
+ if type(req) in (DSL_REQ, RLS_REQ):
+ RES = DSL_RES if type(req) == DSL_REQ else RLS_RES
+ try:
+ self.send_res_recv_req(RES(self.did), 0)
+ except nfc.clf.CommunicationError:
+ pass
+ return
+ if type(req) == DEP_REQ:
+ if req.pfb.fmt == DEP_REQ.Attention:
+ res = ATN(self.did, self.nad)
+ else:
+ res = INF(req.pfb.pni, data, self.did, self.nad)
+ continue
+ res = None
+
+ def exchange(self, send_data, timeout):
+ def INF(pni, data, more, did, nad):
+ pdu_type = (DEP_RES.LastInformation, DEP_RES.MoreInformation)[more]
+ pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, pni)
+ return DEP_RES(pfb, did, nad, data)
+
+ def ACK(pni, did, nad):
+ pdu_type = DEP_RES.PositiveAck
+ pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, pni)
+ return DEP_RES(pfb, did, nad, data=None)
+
+ if send_data is not None and len(send_data) == 0:
+ raise ValueError("send_data must not be empty")
+
+ deadline = time.time() + timeout
+
+ if self.cmd is not None:
+ # first command frame received in activate is injected in
+ # send_res_recv_req and self.cmd then set to None
+ assert send_data is None, "send_data should be None on first call"
+ req = self.send_dep_res_recv_dep_req(None, deadline)
+ self.pni = 0
+ else:
+ send_data = bytearray(send_data)
+ while send_data:
+ data = send_data[0:self.miu]
+ more = len(send_data) > self.miu
+ res = INF(self.pni, data, more, self.did, self.nad)
+ req = self.send_dep_res_recv_dep_req(res, deadline)
+ if req is None:
+ return None
+ if more:
+ if req.pfb.fmt is not DEP_REQ.PositiveAck:
+ error = "expected ACK in NFC-DEP chaining"
+ raise nfc.clf.ProtocolError(error)
+ self.pni = (self.pni + 1) & 0x3
+ if req.pfb.pni != self.pni:
+ raise nfc.clf.ProtocolError("wrong NFC-DEP packet number")
+ del send_data[0:self.miu]
+
+ recv_data = bytearray()
+ while req.pfb.fmt == DEP_REQ.MoreInformation:
+ recv_data += req.data
+ res = ACK(self.pni, self.did, self.nad)
+ req = self.send_dep_res_recv_dep_req(res, deadline)
+ if req is None:
+ return None
+ self.pni = (self.pni + 1) & 0x3
+ if req.pfb.pni != self.pni:
+ raise nfc.clf.ProtocolError("wrong NFC-DEP packet number")
+
+ recv_data += req.data
+ return recv_data
+
+ def send_timeout_extension(self, rtox):
+ def RTOX(rtox, did, nad):
+ pdu_type = DEP_RES.TimeoutExtension
+ pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, 0)
+ return DEP_RES(pfb, did, nad, data=bytearray([rtox]))
+
+ res = RTOX(rtox, self.did, self.nad)
+ req = self.send_dep_res_recv_dep_req(res, deadline=time.time()+1)
+ if type(req) == DEP_REQ and req.pfb.fmt == DEP_REQ.TimeoutExtension:
+ return req.data[0] & 0x3F
+
+ def send_dep_res_recv_dep_req(self, dep_res, deadline):
+ def ATN(did, nad):
+ pdu_type = DEP_RES.Attention
+ pfb = DEP_RES.PFB(pdu_type, nad is not None, did is not None, 0)
+ return DEP_RES(pfb, did, nad, data=None)
+
+ res = dep_res
+ dep_req = None
+ while dep_req is None:
+ req = self.send_res_recv_req(res, deadline)
+ if req is None:
+ return None
+ elif req.did != self.did:
+ log.debug("ignore non-matching device identifier")
+ res = None
+ elif type(req) == DSL_REQ:
+ return self.send_res_recv_req(DSL_RES(self.did), 0)
+ elif type(req) == RLS_REQ:
+ return self.send_res_recv_req(RLS_RES(self.did), 0)
+ elif type(req) == DEP_REQ:
+ if req.pfb.fmt == DEP_REQ.Attention:
+ res = ATN(self.did, self.nad)
+ elif req.pfb.fmt == DEP_REQ.NegativeAck:
+ res = dep_res
+ elif req.pfb.fmt == DEP_REQ.TimeoutExtension:
+ dep_req = req
+ elif req.pfb.pni == self.pni:
+ res = dep_res
+ else:
+ dep_req = req
+ else:
+ log.debug("invalid command in data exchange context")
+ res = None
+ return dep_req
+
+ def send_res_recv_req(self, res, deadline):
+ frame = None
+
+ if self.cmd is not None:
+ # first command is received in activate
+ frame, self.cmd = self.cmd, None
+ else:
+ if res is not None:
+ log.debug(">> {0}".format(res))
+ pcnt_key = res.PDU_NAME[:3]
+ if isinstance(res, DEP_RES):
+ pcnt_key += " " + res.pfb.FMT_NAME
+ self.pcnt.sent[pcnt_key] += 1
+ frame = self.encode_frame(res)
+ while True:
+ timeout = deadline-time.time() if deadline > time.time() else 0
+ try:
+ frame = self.clf.exchange(frame, timeout=timeout)
+ except nfc.clf.TransmissionError:
+ frame = None
+ else:
+ break
+
+ if frame:
+ req = self.decode_frame(frame)
+ log.debug("<< {0}".format(req))
+ pcnt_key = req.PDU_NAME[:3]
+ if isinstance(req, DEP_REQ):
+ pcnt_key += " " + req.pfb.FMT_NAME
+ self.pcnt.rcvd[pcnt_key] += 1
+ return req
+
+ def encode_frame(self, packet):
+ frame = packet.encode()
+ frame = struct.pack("B", len(frame) + 1) + frame
+ if self.target.brty == '106A':
+ frame = b'\xF0' + frame
+ return bytearray(frame)
+
+ def decode_frame(self, frame):
+ if self.target.brty == '106A' and frame.pop(0) != 0xF0:
+ error = "first NFC-DEP frame byte must be F0h for 106A"
+ raise nfc.clf.ProtocolError(error)
+ if len(frame) != frame.pop(0):
+ error = "NFC-DEP frame length byte must be data length + 1"
+ raise nfc.clf.ProtocolError(error)
+ if len(frame) < 2:
+ error = "NFC-DEP frame length byte must be from 3 to 255"
+ raise nfc.clf.TransmissionError(error)
+ if frame[0] != 0xD4 or frame[1] not in (0, 4, 6, 8, 10):
+ raise nfc.clf.ProtocolError("invalid NFC-DEP command code")
+ req_name = {0: 'ATR', 4: 'PSL', 6: 'DEP', 8: 'DSL', 10: 'RLS'}
+ return eval(req_name[frame[1]] + "_REQ").decode(frame)
+
+
+#
+# Data Exchange Protocol Data Units
+#
+class ATR_REQ_RES(object):
+ def __str__(self):
+ nfcid3, gb = [hexlify(ba).decode() for ba in [self.nfcid3, self.gb]]
+ return self.PDU_SHOW.format(self=self, nfcid3=nfcid3, gb=gb)
+
+ @property
+ def lr(self):
+ return (64, 128, 192, 254)[(self.pp >> 4) & 0x3]
+
+
+class ATR_REQ(ATR_REQ_RES):
+ PDU_CODE = bytearray(b'\xD4\x00')
+ PDU_NAME = 'ATR-REQ'
+ PDU_SHOW = "{self.PDU_NAME} NFCID3={nfcid3} DID={self.did:02x} "\
+ "BS={self.bs:02x} BR={self.br:02x} PP={self.pp:02x} GB={gb}"
+
+ def __init__(self, nfcid3, did, bs, br, pp, gb):
+ self.nfcid3, self.did, self.bs, self.br, self.pp, self.gb = \
+ nfcid3, did, bs, br, pp, gb
+
+ def __len__(self):
+ return 16 + len(self.gb)
+
+ @staticmethod
+ def decode(data):
+ if data.startswith(ATR_REQ.PDU_CODE):
+ nfcid3, (did, bs, br, pp) = data[2:12], data[12:16]
+ gb = data[16:] if pp & 0x02 else bytearray()
+ return ATR_REQ(nfcid3, did, bs, br, pp, gb)
+
+ def encode(self):
+ data = ATR_REQ.PDU_CODE + self.nfcid3
+ data.extend([self.did, self.bs, self.br, self.pp])
+ return data + self.gb
+
+
+class ATR_RES(ATR_REQ_RES):
+ PDU_CODE = bytearray(b'\xD5\x01')
+ PDU_NAME = 'ATR-RES'
+ PDU_SHOW = "{self.PDU_NAME} NFCID3={nfcid3} DID={self.did:02x} "\
+ "BS={self.bs:02x} BR={self.br:02x} TO={self.to:02x} "\
+ "PP={self.pp:02x} GB={gb}"
+
+ def __init__(self, nfcid3, did, bs, br, to, pp, gb):
+ self.nfcid3, self.did, self.bs, self.br, self.to, self.pp, self.gb = \
+ nfcid3, did, bs, br, to, pp, gb
+
+ def __len__(self):
+ return 17 + len(self.gb)
+
+ @staticmethod
+ def decode(data):
+ if data.startswith(ATR_RES.PDU_CODE):
+ nfcid3, (did, bs, br, to, pp) = data[2:12], data[12:17]
+ gb = data[17:] if pp & 0x02 else bytearray()
+ return ATR_RES(nfcid3, did, bs, br, to, pp, gb)
+
+ def encode(self):
+ data = ATR_RES.PDU_CODE + self.nfcid3
+ data.extend([self.did, self.bs, self.br, self.to, self.pp])
+ return data + self.gb
+
+ @property
+ def wt(self):
+ return self.to & 0x0F
+
+
+class PSL_REQ_RES(object):
+ def __str__(self):
+ return self.PDU_SHOW.format(name=self.PDU_NAME, self=self)
+
+ @classmethod
+ def decode(cls, data):
+ if data.startswith(cls.PDU_CODE):
+ try:
+ return cls(*data[2:])
+ except TypeError:
+ errstr = "invalid format of the " + cls.PDU_NAME
+ raise nfc.clf.ProtocolError(errstr)
+
+
+class PSL_REQ(PSL_REQ_RES):
+ PDU_CODE = bytearray(b'\xD4\x04')
+ PDU_NAME = 'PSL-REQ'
+ PDU_SHOW = "{name} DID={self.did:02x} BRS={self.brs:02x} " \
+ "FSL={self.fsl:02x}"
+
+ def __init__(self, did, brs, fsl):
+ self.did, self.brs, self.fsl = did if did else 0, brs, fsl
+
+ def encode(self):
+ return PSL_REQ.PDU_CODE + bytearray([self.did, self.brs, self.fsl])
+
+ @property
+ def dsi(self):
+ return self.brs >> 3 & 0x07
+
+ @property
+ def dri(self):
+ return self.brs & 0x07
+
+ @property
+ def lr(self):
+ return (64, 128, 192, 254)[self.fsl & 0x03]
+
+
+class PSL_RES(PSL_REQ_RES):
+ PDU_CODE = bytearray(b'\xD5\x05')
+ PDU_NAME = 'PSL-RES'
+ PDU_SHOW = "{name} DID={self.did:02x}"
+
+ def __init__(self, did):
+ self.did = did
+
+ def encode(self):
+ return PSL_RES.PDU_CODE + bytearray([self.did])
+
+
+class DEP_REQ_RES(object):
+ PDU_SHOW = "{self.PDU_NAME} {self.pfb.FMT_NAME} PNI={self.pfb.pni} "\
+ "DID={self.did} NAD={self.nad} DATA={data}"
+
+ class PFB:
+ def __init__(self, fmt, nad, did, pni):
+ self.fmt, self.nad, self.did, self.pni = fmt, nad, did, pni
+
+ @property
+ def FMT_NAME(self):
+ return {0: "INF", 1: "I++", 4: "ACK", 5: "NAK", 8: "ATN",
+ 9: "TOX"}.get(self.fmt, "{0:04b}".format(self.fmt))
+
+ @property
+ def type(self): return self.fmt
+
+ LastInformation, MoreInformation, PositiveAck, NegativeAck,\
+ Attention, TimeoutExtension = (0, 1, 4, 5, 8, 9)
+
+ def __init__(self, pfb, did, nad, data):
+ self.pfb, self.did, self.nad = pfb, did, nad
+ self.data = bytearray() if data is None else data
+
+ def __str__(self):
+ data = hexlify(self.data).decode()
+ return self.PDU_SHOW.format(self=self, data=data)
+
+ def bytes(self):
+ data = hexlify(self.data)
+ return self.PDU_SHOW.format(self=self, data=data)
+
+ @classmethod
+ def decode(cls, data):
+ if data.startswith(cls.PDU_CODE):
+ del data[0:2]
+ try:
+ pfb = data.pop(0)
+ pfb = cls.PFB(pfb >> 4, bool(pfb & 8), bool(pfb & 4), pfb & 3)
+ did = data.pop(0) if pfb.did else None
+ nad = data.pop(0) if pfb.nad else None
+ except IndexError:
+ errstr = "invalid format of the " + cls.PDU_NAME
+ raise nfc.clf.ProtocolError(errstr)
+ return cls(pfb, did, nad, data)
+
+ def encode(self):
+ pfb = self.pfb
+ pfb = (pfb.fmt << 4) | (pfb.nad << 3) | (pfb.did << 2) | (pfb.pni)
+ data = self.PDU_CODE + struct.pack("B", pfb)
+ if self.pfb.did:
+ data.append(self.did)
+ if self.pfb.nad:
+ data.append(self.nad)
+ return data + self.data
+
+
+class DEP_REQ(DEP_REQ_RES):
+ PDU_CODE = bytearray(b'\xD4\x06')
+ PDU_NAME = 'DEP-REQ'
+
+
+class DEP_RES(DEP_REQ_RES):
+ PDU_CODE = bytearray(b'\xD5\x07')
+ PDU_NAME = 'DEP-RES'
+
+
+class DSL_REQ_RES(object):
+ def __init__(self, did):
+ self.did = did
+
+ def __str__(self):
+ return "{0} DID={1}".format(self.PDU_NAME, self.did)
+
+ @classmethod
+ def decode(cls, data):
+ if data.startswith(cls.PDU_CODE):
+ if len(data) > 3:
+ errstr = "invalid format of the " + cls.PDU_NAME
+ raise nfc.clf.ProtocolError(errstr)
+ return cls(data[2] if len(data) == 3 else None)
+
+ def encode(self):
+ return self.PDU_CODE + (b""
+ if self.did is None
+ else struct.pack("B", self.did))
+
+
+class DSL_REQ(DSL_REQ_RES):
+ PDU_CODE = bytearray(b'\xD4\x08')
+ PDU_NAME = 'DSL-REQ'
+
+
+class DSL_RES(DSL_REQ_RES):
+ PDU_CODE = bytearray(b'\xD5\x09')
+ PDU_NAME = 'DSL-RES'
+
+
+class RLS_REQ_RES(DSL_REQ_RES):
+ pass
+
+
+class RLS_REQ(RLS_REQ_RES):
+ PDU_CODE = bytearray(b'\xD4\x0A')
+ PDU_NAME = 'RLS-REQ'
+
+
+class RLS_RES(RLS_REQ_RES):
+ PDU_CODE = bytearray(b'\xD5\x0B')
+ PDU_NAME = 'RLS-RES'
diff --git a/src/lib/nfc/handover/__init__.py b/src/lib/nfc/handover/__init__.py
new file mode 100644
index 0000000..e887b4d
--- /dev/null
+++ b/src/lib/nfc/handover/__init__.py
@@ -0,0 +1,29 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2012 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""
+The nfc.handover module implements the NFC Forum Connection Handover
+1.2 protocol as a server and client class that simplify realization of
+handover selector and requester functionality.
+
+"""
+from src.lib.nfc.handover.server import HandoverServer # noqa: F401
+from src.lib.nfc.handover.client import HandoverClient # noqa: F401
diff --git a/src/lib/nfc/handover/client.py b/src/lib/nfc/handover/client.py
new file mode 100644
index 0000000..2715e76
--- /dev/null
+++ b/src/lib/nfc/handover/client.py
@@ -0,0 +1,118 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+#
+# Negotiated Connection Handover - Client Base Class
+#
+import binascii
+import logging
+import time
+import ndef
+import src.lib.nfc
+
+
+log = logging.getLogger(__name__)
+
+
+class HandoverClient(object):
+ """ NFC Forum Connection Handover client
+ """
+ def __init__(self, llc):
+ self.socket = None
+ self.llc = llc
+
+ def connect(self, recv_miu=248, recv_buf=2):
+ """Connect to the remote handover server if available. Raises
+ :exc:`nfc.llcp.ConnectRefused` if the remote device does not
+ have a handover service or the service does not accept any
+ more connections."""
+ socket = nfc.llcp.Socket(self.llc, nfc.llcp.DATA_LINK_CONNECTION)
+ socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf)
+ socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu)
+ socket.connect("urn:nfc:sn:handover")
+ server = socket.getpeername()
+ log.debug("handover client connected to remote sap {0}".format(server))
+ self.socket = socket
+
+ def close(self):
+ """Disconnect from the remote handover server."""
+ if self.socket:
+ self.socket.close()
+ self.socket = None
+
+ def send_records(self, records):
+ """Send handover request message records to the remote server."""
+ log.debug("sending '{0}' message".format(records[0].type))
+ try:
+ octets = b''.join(ndef.message_encoder(records))
+ except ndef.EncodeError as error:
+ log.error(repr(error))
+ else:
+ return self.send_octets(octets)
+
+ def send_octets(self, octets):
+ log.debug(">>> %s", binascii.hexlify(octets).decode())
+ miu = self.socket.getsockopt(nfc.llcp.SO_SNDMIU)
+ while len(octets) > 0:
+ if self.socket.send(octets[0:miu]):
+ octets = octets[miu:]
+ else:
+ break
+ return len(octets) == 0
+
+ def recv_records(self, timeout=None):
+ """Receive a handover select message from the remote server."""
+ octets = self.recv_octets(timeout)
+ records = list(ndef.message_decoder(octets, 'relax')) if octets else []
+ if records and records[0].type == "urn:nfc:wkt:Hs":
+ log.debug("received '{0}' message".format(records[0].type))
+ return list(ndef.message_decoder(octets, 'relax'))
+ else:
+ log.error("received invalid message %s", binascii.hexlify(octets))
+ return []
+
+ def recv_octets(self, timeout=None):
+ octets = bytearray()
+ started = time.time()
+ while self.socket.poll("recv", timeout):
+ try:
+ octets += self.socket.recv()
+ except TypeError:
+ log.debug("data link connection closed")
+ return b'' # recv() returned None
+ try:
+ list(ndef.message_decoder(octets, 'strict', {}))
+ log.debug("<<< %s", binascii.hexlify(octets).decode())
+ return bytes(octets)
+ except ndef.DecodeError:
+ log.debug("message is incomplete (%d byte)", len(octets))
+ if timeout:
+ timeout -= time.time() - started
+ started = time.time()
+ log.debug("%.3f seconds left to timeout", timeout)
+ continue # incomplete message
+
+ def __enter__(self):
+ self.connect()
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
diff --git a/src/lib/nfc/handover/server.py b/src/lib/nfc/handover/server.py
new file mode 100644
index 0000000..3817c34
--- /dev/null
+++ b/src/lib/nfc/handover/server.py
@@ -0,0 +1,128 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+#
+# Negotiated Connection Handover - Server Base Class
+#
+import threading
+import binascii
+import logging
+import errno
+import ndef
+import src.lib.nfc
+
+
+log = logging.getLogger(__name__)
+
+
+class HandoverServer(threading.Thread):
+ """ NFC Forum Connection Handover server
+ """
+ def __init__(self, llc, request_size_limit=0x10000,
+ recv_miu=1984, recv_buf=15):
+ socket = nfc.llcp.Socket(llc, nfc.llcp.DATA_LINK_CONNECTION)
+ recv_miu = socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu)
+ recv_buf = socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf)
+ socket.bind('urn:nfc:sn:handover')
+ log.info("handover server bound to port {0} (MIU={1}, RW={2})"
+ .format(socket.getsockname(), recv_miu, recv_buf))
+ socket.listen(backlog=2)
+ threading.Thread.__init__(self, name='urn:nfc:sn:handover',
+ target=self.listen, args=(llc, socket))
+
+ def listen(self, llc, socket):
+ log.debug("handover listen thread started")
+ try:
+ while True:
+ client_socket = socket.accept()
+ client_thread = threading.Thread(target=self.serve,
+ args=(client_socket,))
+ client_thread.start()
+ except nfc.llcp.Error as error:
+ (log.debug if error.errno == errno.EPIPE else log.error)(error)
+ finally:
+ socket.close()
+ log.debug("handover listen thread terminated")
+
+ def serve(self, socket):
+ peer_sap = socket.getpeername()
+ log.info("serving handover client on remote sap {0}".format(peer_sap))
+ send_miu = socket.getsockopt(nfc.llcp.SO_SNDMIU)
+ try:
+ while socket.poll("recv"):
+ request = bytearray()
+ while socket.poll("recv"):
+ request += socket.recv()
+
+ if len(request) == 0:
+ continue # need some data
+
+ try:
+ list(ndef.message_decoder(request, 'strict', {}))
+ except ndef.DecodeError:
+ continue # need more data
+
+ response = self._process_request_data(request)
+
+ for offset in range(0, len(response), send_miu):
+ fragment = response[offset:offset + send_miu]
+ if not socket.send(fragment):
+ return # connection closed
+
+ except nfc.llcp.Error as error:
+ (log.debug if error.errno == errno.EPIPE else log.error)(error)
+ finally:
+ socket.close()
+ log.debug("handover serve thread terminated")
+
+ def _process_request_data(self, octets):
+ log.debug("<<< %s", binascii.hexlify(octets).decode())
+ try:
+ records = list(ndef.message_decoder(octets, 'relax'))
+ except ndef.DecodeError as error:
+ log.error(repr(error))
+ return b''
+
+ if records[0].type == 'urn:nfc:wkt:Hr':
+ records = self.process_handover_request_message(records)
+ else:
+ log.error("received unknown request message")
+ records = []
+
+ octets = b''.join(ndef.message_encoder(records))
+ log.debug(">>> %s", binascii.hexlify(octets).decode())
+ return octets
+
+ def process_handover_request_message(self, records):
+ """Process a handover request message. The *records* argument holds a
+ list of :class:`ndef.Record` objects decoded from the received
+ handover request message octets, where the first record type is
+ ``urn:nfc:wkt:Hr``. The method returns a list of :class:`ndef.Record`
+ objects with the first record typ ``urn:nfc:wkt:Hs``.
+
+ This method should be overwritten by a subclass to customize
+ it's behavior. The default implementation returns a
+ :class:`ndef.HandoverSelectRecord` with version ``1.2`` and no
+ alternative carriers.
+
+ """
+ log.warning("default process_request method should be overwritten")
+ return [ndef.HandoverSelectRecord('1.2')]
diff --git a/src/lib/nfc/llcp/__init__.py b/src/lib/nfc/llcp/__init__.py
new file mode 100644
index 0000000..74631ac
--- /dev/null
+++ b/src/lib/nfc/llcp/__init__.py
@@ -0,0 +1,38 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""
+The nfc.llcp module implements the NFC Forum Logical Link Control
+Protocol (LLCP) specification and provides a socket interface to use
+the connection-less and connection-mode transport facilities of LLCP.
+"""
+from .socket import Socket # noqa: F401
+from .llc import LOGICAL_DATA_LINK, DATA_LINK_CONNECTION # noqa: F401
+from .err import Error, ConnectRefused, errno # noqa: F401
+
+SO_SNDMIU = 1
+SO_RCVMIU = 2
+SO_SNDBUF = 3
+SO_RCVBUF = 4
+SO_SNDBSY = 5
+SO_RCVBSY = 6
+
+MSG_DONTWAIT = 0b00000001
diff --git a/src/lib/nfc/llcp/err.py b/src/lib/nfc/llcp/err.py
new file mode 100644
index 0000000..1a0a498
--- /dev/null
+++ b/src/lib/nfc/llcp/err.py
@@ -0,0 +1,42 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import os
+import errno
+
+
+class Error(IOError):
+ def __init__(self, errno):
+ super(Error, self).__init__(errno, os.strerror(errno))
+
+ def __str__(self):
+ return "nfc.llcp.Error: [{0}] {1}".format(
+ errno.errorcode[self.errno], self.strerror)
+
+
+class ConnectRefused(Error):
+ def __init__(self, reason):
+ super(ConnectRefused, self).__init__(errno.ECONNREFUSED)
+ self.reason = reason
+
+ def __str__(self):
+ return "nfc.llcp.ConnectRefused: [{0}] {1} with reason {2}".format(
+ errno.errorcode[self.errno], self.strerror, self.reason)
diff --git a/src/lib/nfc/llcp/llc.py b/src/lib/nfc/llcp/llc.py
new file mode 100644
index 0000000..d92fcbc
--- /dev/null
+++ b/src/lib/nfc/llcp/llc.py
@@ -0,0 +1,886 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+from . import tco
+from . import pdu
+from . import err
+from . import sec
+import src.lib.nfc.llcp
+import src.lib.nfc.clf
+import src.lib.nfc.dep
+
+import re
+import time
+import errno
+import random
+import threading
+import collections
+
+import logging
+log = logging.getLogger(__name__)
+
+RAW_ACCESS_POINT, LOGICAL_DATA_LINK, DATA_LINK_CONNECTION = range(3)
+
+wks_map = {
+ b"urn:nfc:sn:sdp": 1,
+ b"urn:nfc:sn:snep": 4,
+}
+
+service_name_format = \
+ re.compile(b"^urn:nfc:[x]?sn:[a-zA-Z][a-zA-Z0-9-_:\\.]*$")
+
+
+class ServiceAccessPoint(object):
+ def __init__(self, addr, llc):
+ self.llc = llc
+ self.addr = addr
+ self.sock_list = collections.deque()
+ self.send_list = collections.deque()
+
+ def __str__(self):
+ return "SAP {0:>2}".format(self.addr)
+
+ @property
+ def mode(self):
+ with self.llc.lock:
+ try:
+ if isinstance(self.sock_list[0], tco.RawAccessPoint):
+ return RAW_ACCESS_POINT
+ if isinstance(self.sock_list[0], tco.LogicalDataLink):
+ return LOGICAL_DATA_LINK
+ if isinstance(self.sock_list[0], tco.DataLinkConnection):
+ return DATA_LINK_CONNECTION
+ except IndexError:
+ return 0
+
+ def insert_socket(self, socket):
+ with self.llc.lock:
+ try:
+ insertable = isinstance(socket, type(self.sock_list[0]))
+ except IndexError:
+ insertable = True
+ if insertable:
+ socket.bind(self.addr)
+ self.sock_list.appendleft(socket)
+ else:
+ log.error("can't insert socket of different type")
+ return insertable
+
+ def remove_socket(self, socket):
+ assert socket.addr == self.addr
+ socket.close()
+ with self.llc.lock:
+ try:
+ self.sock_list.remove(socket)
+ except ValueError:
+ pass
+ if len(self.sock_list) == 0:
+ # completely remove this sap
+ self.llc.sap[self.addr] = None
+
+ def send(self, send_pdu):
+ self.send_list.append(send_pdu)
+
+ def shutdown(self):
+ while True:
+ try:
+ socket = self.sock_list.pop()
+ except IndexError:
+ return
+ log.debug("shutdown socket %s" % str(socket))
+ socket.bind(None)
+ socket.close()
+
+ #
+ # enqueue() and dequeue() are called from llc run thread
+ #
+ def enqueue(self, rcvd_pdu):
+ with self.llc.lock:
+ if isinstance(rcvd_pdu, pdu.Connect):
+ for socket in self.sock_list:
+ if socket.state.LISTEN:
+ socket.enqueue(rcvd_pdu)
+ break
+ else:
+ args = (rcvd_pdu.ssap, rcvd_pdu.dsap, 0x02)
+ self.send(pdu.DisconnectedMode(*args))
+ else:
+ for socket in self.sock_list:
+ if rcvd_pdu.ssap == socket.peer or socket.peer is None:
+ socket.enqueue(rcvd_pdu)
+ break
+ else:
+ if rcvd_pdu.name in tco.DataLinkConnection.DLC_PDU_NAMES:
+ args = (rcvd_pdu.ssap, rcvd_pdu.dsap, 0x01)
+ self.send(pdu.DisconnectedMode(*args))
+ else:
+ log.debug("%s discard PDU %s", self, rcvd_pdu)
+
+ def dequeue(self, miu_size, icv_size):
+ with self.llc.lock:
+ for socket in self.sock_list:
+ send_pdu = socket.dequeue(miu_size, icv_size)
+ if send_pdu:
+ return send_pdu
+ else:
+ try:
+ return self.send_list.popleft()
+ except IndexError:
+ pass
+
+ def sendack(self):
+ with self.llc.lock:
+ for socket in self.sock_list:
+ send_pdu = socket.sendack()
+ if send_pdu:
+ return send_pdu
+
+
+class ServiceDiscovery(object):
+ def __init__(self, llc):
+ self.llc = llc
+ self.snl = dict()
+ self.tids = list(range(256))
+ self.resp = threading.Condition(self.llc.lock)
+ self.sent = dict()
+ self.sdreq = collections.deque()
+ self.sdres = collections.deque()
+ self.dmpdu = collections.deque()
+
+ def __str__(self):
+ return "SAP 1"
+
+ @property
+ def mode(self):
+ return LOGICAL_DATA_LINK
+
+ def resolve(self, name):
+ with self.resp:
+ if self.snl is None:
+ return None
+ log.debug("resolve service name %r", name)
+ try:
+ return self.snl[name]
+ except KeyError:
+ pass
+ tid = random.choice(self.tids)
+ self.tids.remove(tid)
+ self.sdreq.append((tid, name))
+ while self.snl is not None and name not in self.snl:
+ self.resp.wait()
+ return None if self.snl is None else self.snl[name]
+
+ #
+ # enqueue() and dequeue() are called from llc run thread
+ #
+ def enqueue(self, rcvd_pdu):
+ with self.llc.lock:
+ if ((isinstance(rcvd_pdu, pdu.ServiceNameLookup)
+ and self.snl is not None)):
+
+ for tid, sap in rcvd_pdu.sdres:
+ try:
+ name = self.sent[tid]
+ except KeyError:
+ continue
+ log.debug("resolved %r to remote addr %d", name, sap)
+ csn, sap = sap >> 6 & 1, sap & 63
+ if csn:
+ sap = 1
+ self.snl[name] = sap
+ self.tids.append(tid)
+ self.resp.notify_all()
+
+ for tid, name in rcvd_pdu.sdreq:
+ try:
+ sap = self.llc.snl[name]
+ except KeyError:
+ sap = 0
+ self.sdres.append((tid, sap))
+
+ def dequeue(self, miu_size, icv_size):
+ with self.llc.lock:
+ if len(self.sdres) > 0 or len(self.sdreq) > 0:
+ send_pdu = pdu.ServiceNameLookup(dsap=1, ssap=1)
+ # add service discovery responses
+ while miu_size > 0:
+ try:
+ send_pdu.sdres.append(self.sdres.popleft())
+ miu_size -= 4
+ except IndexError:
+ break
+ # add service discovery requests
+ for i in range(len(self.sdreq)):
+ tid, name = self.sdreq[0]
+ if 3 + len(name) > miu_size:
+ self.sdreq.rotate(-1)
+ else:
+ send_pdu.sdreq.append(self.sdreq.popleft())
+ self.sent[tid] = name
+ miu_size -= 3 + len(name)
+ return send_pdu
+ if len(self.dmpdu) > 0 and miu_size > 0:
+ return self.dmpdu.popleft()
+
+ def shutdown(self):
+ with self.llc.lock:
+ self.snl = None
+ self.resp.notify_all()
+
+
+class LogicalLinkController(object):
+ class LinkState(object):
+ def __init__(self):
+ self.names = ("SHUTDOWN", "LISTEN", "CONNECT", "CONNECTED",
+ "ESTABLISHED", "DISCONNECT", "CLOSED")
+ self.value = self.names.index("SHUTDOWN")
+
+ def __str__(self):
+ return self.names[self.value]
+
+ def __getattr__(self, name):
+ return self.value == self.names.index(name)
+
+ def __setattr__(self, name, value):
+ if name not in ("names", "value"):
+ value, name = self.names.index(name), "value"
+ parent = super(LogicalLinkController.LinkState, self)
+ parent.__setattr__(name, value)
+
+ class Counter(object):
+ def __init__(self):
+ self.sent = collections.defaultdict(int)
+ self.rcvd = collections.defaultdict(int)
+
+ @property
+ def sent_count(self):
+ return sum(self.sent.values())
+
+ @property
+ def rcvd_count(self):
+ return sum(self.rcvd.values())
+
+ def __str__(self):
+ s = "sent/rcvd {0}/{1}".format(self.sent_count, self.rcvd_count)
+ for name in sorted(set(list(self.sent.keys())
+ + list(self.rcvd.keys()))):
+ s += " {name} {sent}/{rcvd}".format(
+ name=name, sent=self.sent[name], rcvd=self.rcvd[name])
+ return s
+
+ def __init__(self, **options):
+ self.pcnt = LogicalLinkController.Counter()
+ self.link = LogicalLinkController.LinkState()
+ self.lock = threading.RLock()
+ self.cfg = dict()
+ self.cfg['recv-miu'] = options.get('miu', 248)
+ self.cfg['send-lto'] = options.get('lto', 500)
+ self.cfg['send-lsc'] = options.get('lsc', 3)
+ self.cfg['send-agf'] = options.get('agf', True)
+ self.cfg['llcp-sec'] = options.get('sec', True)
+ if not sec.OpenSSL:
+ self.cfg['llcp-sec'] = False
+ log.debug("llc cfg {0}".format(self.cfg))
+ self.sec = None
+ self.snl = dict({b"urn:nfc:sn:sdp": 1})
+ self.sap = 64 * [None]
+ self.sap[0] = ServiceAccessPoint(0, self)
+ self.sap[1] = ServiceDiscovery(self)
+
+ def __str__(self):
+ local = "Local(MIU={miu}, LTO={lto}ms)".format(
+ miu=self.cfg.get('recv-miu'), lto=self.cfg.get('send-lto'))
+ remote = "Remote(MIU={miu}, LTO={lto}ms)".format(
+ miu=self.cfg.get('send-miu'), lto=self.cfg.get('recv-lto'))
+ return "LLC: {local} {remote}".format(local=local, remote=remote)
+
+ @property
+ def secure_data_transfer(self):
+ return self.cfg.get('llcp-dpc', 0) == 1
+
+ def activate(self, mac, **options):
+ assert isinstance(mac, (nfc.dep.Initiator, nfc.dep.Target))
+ self.mac = None
+
+ wks = 1 + sum([1 << sap for sap in self.snl.values() if sap < 15])
+
+ send_pax = pdu.ParameterExchange()
+ send_pax.version = (1, 3)
+ send_pax.wks = wks
+ if self.cfg['recv-miu'] != 128:
+ send_pax.miu = self.cfg['recv-miu']
+ if self.cfg['send-lto'] != 100:
+ send_pax.lto = self.cfg['send-lto']
+ if self.cfg['send-lsc'] != 0:
+ send_pax.lsc = self.cfg['send-lsc']
+ if self.cfg['llcp-sec']:
+ send_pax.dpc = 1
+
+ gb = b'Ffm' + pdu.encode(send_pax)[2:]
+ if isinstance(mac, nfc.dep.Initiator):
+ self.link.CONNECT = True
+ gb = mac.activate(gbi=gb, **options)
+ self.run = self.run_as_initiator
+ else:
+ self.link.LISTEN = True
+ gb = mac.activate(gbt=gb, **options)
+ self.run = self.run_as_target
+
+ if gb and gb.startswith(b'Ffm') and len(gb) >= 6:
+ if ((isinstance(mac, nfc.dep.Target)
+ and mac.rwt >= send_pax.lto * 1E-3)):
+ msg = "local NFC-DEP RWT {0:.3f} contradicts LTO {1:.3f} sec"
+ log.warning(msg.format(mac.rwt, send_pax.lto*1E3))
+
+ rcvd_pax = pdu.decode(b"\x00\x40" + bytes(gb[3:]))
+
+ log.debug("SENT {0}".format(send_pax))
+ log.debug("RCVD {0}".format(rcvd_pax))
+
+ self.cfg['rcvd-ver'] = rcvd_pax.version
+ self.cfg['send-miu'] = rcvd_pax.miu
+ self.cfg['recv-lto'] = rcvd_pax.lto
+ self.cfg['send-wks'] = rcvd_pax.wks
+ self.cfg['send-lsc'] = rcvd_pax.lsc
+ self.cfg['llcp-dpc'] = rcvd_pax.dpc if self.cfg['llcp-sec'] else 0
+ log.debug("llc cfg {0}".format(self.cfg))
+
+ info = '\n'.join([
+ "LLCP Link established as NFC-DEP {role}",
+ "Local LLCP Settings",
+ " LLCP Version: {send_pax.version_text}",
+ " Link Timeout: {send_pax.lto} ms",
+ " Max Inf Unit: {send_pax.miu} octet",
+ " Link Service: {send_pax.lsc_text}",
+ " Data Protect: {send_pax.dpc_text}",
+ " Service List: {send_pax.wks:016b} ({send_pax.wks_text})",
+ "Remote LLCP Settings",
+ " LLCP Version: {rcvd_pax.version[0]}.{rcvd_pax.version[1]}",
+ " Link Timeout: {rcvd_pax.lto} ms",
+ " Max Inf Unit: {rcvd_pax.miu} octet",
+ " Link Service: {rcvd_pax.lsc_text}",
+ " Data Protect: {rcvd_pax.dpc_text}",
+ " Service List: {rcvd_pax.wks:016b} ({rcvd_pax.wks_text})"
+ ]).format(role=mac.role, send_pax=send_pax, rcvd_pax=rcvd_pax)
+ log.info(info)
+
+ if isinstance(mac, nfc.dep.Initiator) and mac.rwt is not None:
+ max_rwt = 4096/13.56E6 * 2**10
+ if mac.rwt > max_rwt:
+ msg = "remote NFC-DEP RWT {0:.3f} exceeds max {1:.3f} sec"
+ log.warning(msg.format(mac.rwt, max_rwt))
+
+ self.mac = mac
+ self.link.CONNECTED = True
+
+ return bool(self.mac)
+
+ def terminate(self, reason):
+ log.debug("llcp link termination caused by {0}".format(reason))
+ if type(self.mac) == nfc.dep.Initiator:
+ if self.link.DISCONNECT is True:
+ self.exchange(pdu.Disconnect(0, 0), timeout=0.5)
+ self.mac.deactivate(release=False) # use DESELECT
+ if type(self.mac) == nfc.dep.Target:
+ self.mac.deactivate(data=bytearray(b"\x01\x40"))
+ # shutdown local services
+ for i in range(63, -1, -1):
+ if not self.sap[i] is None:
+ log.debug("closing service access point %d" % i)
+ self.sap[i].shutdown()
+ self.sap[i] = None
+ self.link.SHUTDOWN = True
+
+ def exchange(self, send_pdu, timeout):
+ # Send and receive one protocol data unit. The send_pdu is
+ # None for the first call when running as target (because the
+ # target first receives a pdu). All PDUs except SYMM are
+ # logged with debug level, SYMM is logged with DEBUG-1 so that
+ # it must be explicitely enabled. The return value is either a
+ # PDU instance or None.
+ try:
+ if send_pdu:
+ loglevel = logging.DEBUG - bool(send_pdu.name == "SYMM")
+ log.log(loglevel, "SEND %s", send_pdu)
+ send_data = pdu.encode(send_pdu)
+ self.pcnt.sent[send_pdu.name] += 1
+ rcvd_data = self.mac.exchange(send_data, timeout)
+ else:
+ rcvd_data = self.mac.exchange(None, timeout)
+ if rcvd_data is not None:
+ rcvd_pdu = pdu.decode(rcvd_data)
+ self.pcnt.rcvd[rcvd_pdu.name] += 1
+ loglevel = logging.DEBUG - bool(rcvd_pdu.name == "SYMM")
+ log.log(loglevel, "RECV %s", rcvd_pdu)
+ return rcvd_pdu
+ except (nfc.clf.CommunicationError, pdu.Error) as error:
+ log.warning("{0!r}".format(error))
+
+ def run_as_initiator(self, terminate=lambda: False):
+ recv_timeout = 1E-3 * (self.cfg['recv-lto'] + 10)
+ msg = "starting initiator run loop with a receive timeout of %.3f sec"
+ log.debug(msg, recv_timeout)
+
+ symm = 0 # counts the number of consecutive SYMM PDUs
+ try:
+ if self.cfg['llcp-dpc'] == 1:
+ cipher = sec.cipher_suite("ECDH_anon_WITH_AEAD_AES_128_CCM_4")
+ pubkey = cipher.public_key_x + cipher.public_key_y
+ random = cipher.random_nonce
+ send_dps = pdu.DataProtectionSetup(0, 0, pubkey, random)
+ rcvd_dps = self.exchange(send_dps, recv_timeout)
+ if not isinstance(rcvd_dps, pdu.DataProtectionSetup):
+ log.error("expected a DPS PDU response")
+ return self.terminate(reason="key agreement error")
+ if not (rcvd_dps.ecpk and len(rcvd_dps.ecpk) == 64):
+ log.error("absent or invalid ECPK parameter in DPS PDU")
+ return self.terminate(reason="key agreement error")
+ if not (rcvd_dps.rn and len(rcvd_dps.rn) == 8):
+ log.error("absent or invalid RN parameter in DPS PDU")
+ return self.terminate(reason="key agreement error")
+ cipher.calculate_session_key(rcvd_dps.ecpk, rn_t=rcvd_dps.rn)
+ self.sec = cipher
+
+ send_pdu = self.collect(delay=0.01)
+ self.link.ESTABLISHED = True
+ while not terminate():
+ if send_pdu is None:
+ send_pdu = pdu.Symmetry()
+ rcvd_pdu = self.exchange(send_pdu, recv_timeout)
+ if rcvd_pdu is None:
+ return self.terminate(reason="link disruption")
+ if rcvd_pdu == pdu.Disconnect(0, 0):
+ self.link.CLOSED = True
+ return self.terminate(reason="remote choice")
+ symm += 1 if rcvd_pdu.name == "SYMM" else 0
+ self.dispatch(rcvd_pdu)
+ send_pdu = self.collect(delay=0.001)
+ if send_pdu is None and symm >= 10:
+ send_pdu = self.collect(delay=0.05)
+ else:
+ self.link.DISCONNECT = True
+ self.terminate(reason="local choice")
+ except KeyboardInterrupt:
+ print() # move to new line
+ self.link.DISCONNECT = True
+ self.terminate(reason="local choice")
+ raise KeyboardInterrupt
+ except IOError:
+ self.terminate(reason="input/output error")
+ raise SystemExit
+ except sec.KeyAgreementError:
+ self.terminate(reason="key agreement error")
+ raise SystemExit
+ except sec.DecryptionError:
+ self.terminate(reason="decryption error")
+ raise SystemExit
+ except sec.EncryptionError:
+ self.terminate(reason="encryption error")
+ raise SystemExit
+ finally:
+ log.debug("llc run loop terminated on initiator")
+
+ def run_as_target(self, terminate=lambda: False):
+ recv_timeout = 1E-3 * (self.cfg['recv-lto'] + 10)
+ msg = "starting target run loop with a receive timeout of %.3f sec"
+ log.debug(msg, recv_timeout)
+
+ symm = 0 # counts the number of consecutive SYMM PDUs
+ try:
+ if self.cfg['llcp-dpc'] == 1:
+ cipher = sec.cipher_suite("ECDH_anon_WITH_AEAD_AES_128_CCM_4")
+ pubkey = cipher.public_key_x + cipher.public_key_y
+ random = cipher.random_nonce
+ send_dps = pdu.DataProtectionSetup(0, 0, pubkey, random)
+ rcvd_dps = self.exchange(None, recv_timeout)
+ if not isinstance(rcvd_dps, pdu.DataProtectionSetup):
+ log.error("expected a DPS PDU request")
+ return self.terminate(reason="key agreement error")
+ if not (rcvd_dps.ecpk and len(rcvd_dps.ecpk) == 64):
+ log.error("absent or invalid ECPK parameter in DPS PDU")
+ return self.terminate(reason="key agreement error")
+ if not (rcvd_dps.rn and len(rcvd_dps.rn) == 8):
+ log.error("absent or invalid RN parameter in DPS PDU")
+ return self.terminate(reason="key agreement error")
+ rcvd_pdu = self.exchange(send_dps, recv_timeout)
+ cipher.calculate_session_key(rcvd_dps.ecpk, rn_i=rcvd_dps.rn)
+ self.sec = cipher
+ else:
+ rcvd_pdu = self.exchange(None, recv_timeout)
+
+ self.link.ESTABLISHED = True
+ while not terminate():
+ if rcvd_pdu is None:
+ return self.terminate(reason="link disruption")
+ if rcvd_pdu == pdu.Disconnect(0, 0):
+ self.link.CLOSED = True
+ return self.terminate(reason="remote choice")
+ symm += 1 if isinstance(rcvd_pdu, pdu.Symmetry) else 0
+ self.dispatch(rcvd_pdu)
+ send_pdu = self.collect(delay=0.001)
+ if send_pdu is None and symm >= 10:
+ send_pdu = self.collect(delay=0.05)
+ if send_pdu is None:
+ send_pdu = pdu.Symmetry()
+ rcvd_pdu = self.exchange(send_pdu, recv_timeout)
+ else:
+ self.link.DISCONNECT = True
+ self.terminate(reason="local choice")
+ except KeyboardInterrupt:
+ print() # move to new line
+ self.link.DISCONNECT = True
+ self.terminate(reason="local choice")
+ raise KeyboardInterrupt
+ except IOError:
+ self.terminate(reason="input/output error")
+ raise SystemExit
+ except sec.KeyAgreementError:
+ self.terminate(reason="key agreement error")
+ raise SystemExit
+ except sec.DecryptionError:
+ self.terminate(reason="decryption error")
+ raise SystemExit
+ except sec.EncryptionError:
+ self.terminate(reason="encryption error")
+ raise SystemExit
+ finally:
+ log.debug("llc run loop terminated on target")
+
+ def collect(self, delay=None):
+ # Collect a single PDU or multiple PDUs if aggregation is enabled.
+ if delay:
+ time.sleep(delay)
+
+ def encrypt(send_pdu):
+ pdu_type = type(send_pdu)
+ a = send_pdu.encode_header()
+ c = self.sec.encrypt(a, send_pdu.data)
+ return pdu_type(*pdu_type.decode_header(a), data=c)
+
+ miu_size = self.cfg["send-miu"]
+ icv_size = self.sec.icv_size if self.sec else 0
+ send_pdu = None
+
+ with self.lock:
+ # Dequeue from the list of active SAP until a first PDU is
+ # returned. The list is sorted to first iterate the raw
+ # SAPs (raw SAPs do not respect the miu_size value and we
+ # must avoid them to return PDUs in aggregation). The PDU
+ # is returned straight if it fills or exceeds the Link
+ # MIU. Otherwise the loop terminates at this point. The
+ # sap.dequeue method is called with icv_size=0 because for
+ # encrypted but not aggregated UI and I PDUs the receiver
+ # must accept them with complete MIU plus ICV size.
+ for sap in sorted(filter(None, self.sap), reverse=True,
+ key=lambda sap: sap.mode == RAW_ACCESS_POINT):
+ send_pdu = sap.dequeue(miu_size, icv_size=0)
+ if send_pdu:
+ if self.sec and send_pdu.name in ("UI", "I"):
+ send_pdu = encrypt(send_pdu)
+ if len(send_pdu) - send_pdu.header_size >= miu_size:
+ return send_pdu
+ break
+
+ # Data Link Connection endpoints do not dequeue RR/RNR PDUs until
+ # the receive window is exhausted. If there is not yet a PDU to
+ # send, this loop allows voluntary acknowledgement.
+ if send_pdu is None:
+ for sap in filter(None, self.sap):
+ if sap.mode == DATA_LINK_CONNECTION:
+ send_pdu = sap.sendack()
+ if send_pdu:
+ break
+
+ # Finish if either there is either no PDU to send or if PDU
+ # aggregation is disabled.
+ if send_pdu is None or self.cfg['send-agf'] is False:
+ return send_pdu
+
+ # We have one PDU to send and aggregation is enabled. We'll see if
+ # there are more outbound PDUs and collect them into an AGF PDU.
+ agf_pdu = pdu.AggregatedFrame(0, 0, [send_pdu])
+ miu_size = self.cfg["send-miu"] - len(agf_pdu) - 3
+ while True:
+ # The first loop will dequeue PDUs until the reamining miu_size
+ # is exhausted or all active SAP did not return a PDU.
+ deq_none = True
+ for sap in filter(None, self.sap):
+ send_pdu = sap.dequeue(miu_size, icv_size)
+ if send_pdu:
+ deq_none = False
+ if self.sec and send_pdu.name in ("UI", "I"):
+ send_pdu = encrypt(send_pdu)
+ agf_pdu.append(send_pdu)
+ miu_size = self.cfg["send-miu"] - len(agf_pdu) - 3
+ if miu_size < 0:
+ break
+ if miu_size < 0 or deq_none:
+ break
+ # If the miu_size is not yet exhausted we query all data link
+ # connection endpoints once for voluntary acknowledgements.
+ if miu_size >= 0:
+ for sap in filter(None, self.sap):
+ if sap.mode == DATA_LINK_CONNECTION:
+ send_pdu = sap.sendack()
+ if send_pdu:
+ agf_pdu.append(send_pdu)
+ miu_size = self.cfg["send-miu"] - len(agf_pdu) - 3
+ if miu_size < 0:
+ break
+
+ return agf_pdu if agf_pdu.count > 1 else agf_pdu.first
+
+ def dispatch(self, rcvd_pdu):
+ if rcvd_pdu is None or rcvd_pdu.name == "SYMM":
+ return
+
+ if rcvd_pdu.name == "AGF":
+ if rcvd_pdu.dsap == 0 and rcvd_pdu.ssap == 0:
+ for p in rcvd_pdu:
+ log.debug(" " + str(p))
+ for p in rcvd_pdu:
+ self.dispatch(p)
+ return
+
+ if rcvd_pdu.name == "CONNECT" and rcvd_pdu.dsap == 1:
+ # connect-by-name
+ addr = self.snl.get(rcvd_pdu.sn)
+ if not addr or self.sap[addr] is None:
+ dm_reason = 0x10 if rcvd_pdu.sn is None else 0x02
+ dm_pdu = pdu.DisconnectedMode(rcvd_pdu.ssap, 1, dm_reason)
+ self.sap[1].dmpdu.append(dm_pdu)
+ log.debug("could not find service %r", rcvd_pdu.sn)
+ return
+ # service found, rewrite CONNECT PDU to its DSAP
+ rcvd_pdu = pdu.Connect(dsap=addr, ssap=rcvd_pdu.ssap,
+ rw=rcvd_pdu.rw, miu=rcvd_pdu.miu)
+
+ if self.sec and rcvd_pdu.name in ("UI", "I"):
+ pdu_type = type(rcvd_pdu)
+ a = rcvd_pdu.encode_header()
+ p = self.sec.decrypt(a, rcvd_pdu.data)
+ rcvd_pdu = pdu_type(*pdu_type.decode_header(a), data=p)
+
+ with self.lock:
+ sap = self.sap[rcvd_pdu.dsap]
+ if sap:
+ sap.enqueue(rcvd_pdu)
+ else:
+ log.debug("can't dispatch PDU %s", rcvd_pdu)
+
+ def resolve(self, name):
+ if isinstance(name, (bytes, bytearray)):
+ return self.sap[1].resolve(bytes(name))
+ return self.sap[1].resolve(name.encode('latin'))
+
+ def socket(self, socket_type):
+ if socket_type == RAW_ACCESS_POINT:
+ return tco.RawAccessPoint(recv_miu=self.cfg["recv-miu"])
+ if socket_type == LOGICAL_DATA_LINK:
+ return tco.LogicalDataLink(recv_miu=self.cfg["recv-miu"])
+ if socket_type == DATA_LINK_CONNECTION:
+ return tco.DataLinkConnection(recv_miu=128, recv_win=1)
+
+ def setsockopt(self, socket, option, value):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ if option == nfc.llcp.SO_RCVMIU:
+ value = min(value, self.cfg['recv-miu'])
+ socket.setsockopt(option, value)
+ return socket.getsockopt(option)
+
+ def getsockopt(self, socket, option):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ if isinstance(socket, tco.LogicalDataLink):
+ # FIXME: set socket send miu when activated
+ socket.send_miu = self.cfg['send-miu']
+ if isinstance(socket, tco.RawAccessPoint):
+ # FIXME: set socket send miu when activated
+ socket.send_miu = self.cfg['send-miu']
+ return socket.getsockopt(option)
+
+ def bind(self, socket, addr_or_name=None):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ if socket.addr is not None:
+ raise err.Error(errno.EINVAL)
+ if addr_or_name is None:
+ self._bind_by_none(socket)
+ elif isinstance(addr_or_name, int):
+ self._bind_by_addr(socket, addr_or_name)
+ elif isinstance(addr_or_name, (bytes, bytearray)):
+ self._bind_by_name(socket, bytes(addr_or_name))
+ elif isinstance(addr_or_name, str):
+ self._bind_by_name(socket, addr_or_name.encode('latin'))
+ else:
+ raise err.Error(errno.EFAULT)
+
+ def _bind_by_none(self, socket):
+ with self.lock:
+ try:
+ addr = 32 + self.sap[32:64].index(None)
+ except ValueError:
+ raise err.Error(errno.EAGAIN)
+ else:
+ socket.bind(addr)
+ self.sap[addr] = ServiceAccessPoint(addr, self)
+ self.sap[addr].insert_socket(socket)
+
+ def _bind_by_addr(self, socket, addr):
+ if addr < 0 or addr > 63:
+ raise err.Error(errno.EFAULT)
+ with self.lock:
+ if addr in range(32, 64) or isinstance(socket, tco.RawAccessPoint):
+ if self.sap[addr] is None:
+ socket.bind(addr)
+ self.sap[addr] = ServiceAccessPoint(addr, self)
+ self.sap[addr].insert_socket(socket)
+ else:
+ raise err.Error(errno.EADDRINUSE)
+ else:
+ raise err.Error(errno.EACCES)
+
+ def _bind_by_name(self, socket, name):
+ if not service_name_format.match(name):
+ raise err.Error(errno.EFAULT)
+
+ with self.lock:
+ if self.snl.get(name) is not None:
+ raise err.Error(errno.EADDRINUSE)
+ addr = wks_map.get(name)
+ if addr is None:
+ try:
+ addr = 16 + self.sap[16:32].index(None)
+ except ValueError:
+ raise err.Error(errno.EADDRNOTAVAIL)
+ socket.bind(addr)
+ self.sap[addr] = ServiceAccessPoint(addr, self)
+ self.sap[addr].insert_socket(socket)
+ self.snl[name] = addr
+
+ def connect(self, socket, dest):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ if not socket.is_bound:
+ self.bind(socket)
+ socket.connect(dest)
+ log.debug("connected ({0} ===> {1})".format(socket.addr, socket.peer))
+ if socket.send_miu > self.cfg['send-miu']:
+ log.warning("reducing outbound miu to not exceed the link miu")
+ socket.send_miu = self.cfg['send-miu']
+
+ def listen(self, socket, backlog):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ if not isinstance(socket, tco.DataLinkConnection):
+ raise err.Error(errno.EOPNOTSUPP)
+ if not isinstance(backlog, int):
+ raise TypeError("backlog must be int type")
+ if backlog < 0:
+ raise ValueError("backlog can not be negative")
+ backlog = min(backlog, 16)
+ if not socket.is_bound:
+ self.bind(socket)
+ socket.listen(backlog)
+
+ def accept(self, socket):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ if not isinstance(socket, tco.DataLinkConnection):
+ raise err.Error(errno.EOPNOTSUPP)
+ while True:
+ client = socket.accept()
+ self.sap[client.addr].insert_socket(client)
+ log.debug("new data link connection ({0} <=== {1})"
+ .format(client.addr, client.peer))
+ if client.send_miu > self.cfg['send-miu']:
+ log.warning("reducing outbound miu to comply with link miu")
+ client.send_miu = self.cfg['send-miu']
+ return client
+
+ def send(self, socket, message, flags):
+ return self.sendto(socket, message, socket.peer, flags)
+
+ def sendto(self, socket, message, dest, flags):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ if isinstance(socket, tco.RawAccessPoint):
+ if not isinstance(message, pdu.ProtocolDataUnit):
+ raise TypeError("on a raw access point message must be a pdu")
+ if not socket.is_bound:
+ self.bind(socket)
+ # FIXME: set socket send miu when activated
+ socket.send_miu = self.cfg['send-miu']
+ return socket.send(message, flags)
+ if not isinstance(message, (bytes, bytearray)):
+ raise TypeError("message data must be a bytes-like object")
+ if isinstance(socket, tco.LogicalDataLink):
+ if dest is None:
+ raise err.Error(errno.EDESTADDRREQ)
+ if not socket.is_bound:
+ self.bind(socket)
+ # FIXME: set socket send miu when activated
+ socket.send_miu = self.cfg['send-miu']
+ return socket.sendto(message, dest, flags)
+ if isinstance(socket, tco.DataLinkConnection):
+ return socket.send(message, flags)
+
+ def recv(self, socket):
+ message, sender = self.recvfrom(socket)
+ return message
+
+ def recvfrom(self, socket):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ if not (socket.addr and self.sap[socket.addr]):
+ raise err.Error(errno.EBADF)
+ if isinstance(socket, tco.RawAccessPoint):
+ return (socket.recv(), None)
+ if isinstance(socket, tco.LogicalDataLink):
+ return socket.recvfrom()
+ if isinstance(socket, tco.DataLinkConnection):
+ return (socket.recv(), socket.peer)
+
+ def poll(self, socket, event, timeout=None):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ if not (socket.addr and self.sap[socket.addr]):
+ raise err.Error(errno.EBADF)
+ return socket.poll(event, timeout)
+
+ def close(self, socket):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ if socket.is_bound:
+ self.sap[socket.addr].remove_socket(socket)
+ else:
+ socket.close()
+
+ def getsockname(self, socket):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ return socket.addr
+
+ def getpeername(self, socket):
+ if not isinstance(socket, tco.TransmissionControlObject):
+ raise err.Error(errno.ENOTSOCK)
+ return socket.peer
diff --git a/src/lib/nfc/llcp/pdu.py b/src/lib/nfc/llcp/pdu.py
new file mode 100644
index 0000000..1ca160e
--- /dev/null
+++ b/src/lib/nfc/llcp/pdu.py
@@ -0,0 +1,945 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import struct
+from binascii import hexlify
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Error(Exception):
+ pass
+
+
+class DecodeError(Error):
+ pass
+
+
+class EncodeError(Error):
+ pass
+
+
+class Parameter:
+ VERSION, MIUX, WKS, LTO, RW, SN, OPT, SDREQ, SDRES, ECPK, RN = range(1, 12)
+
+ @staticmethod
+ def decode(data, offset):
+ try:
+ T, L = struct.unpack_from('BB', data, offset)
+ V = struct.unpack_from('%ds' % L, data, offset+2)[0]
+ except struct.error as error:
+ msg = " while decoding TLV %r" % hexlify(data[offset:])
+ raise DecodeError(str(error) + msg)
+
+ if T == Parameter.VERSION:
+ if L != 1:
+ raise DecodeError("VERSION TLV length error")
+ V = struct.unpack('B', V)[0]
+ elif T == Parameter.MIUX:
+ if L != 2:
+ raise DecodeError("MIUX TLV length error")
+ V = struct.unpack('>H', V)[0]
+ if V & 0xF800:
+ log.warning("MIUX TLV reserved bits set")
+ V = V & 0x07FF
+ elif T == Parameter.WKS:
+ if L != 2:
+ raise DecodeError("WKS TLV length error")
+ V = struct.unpack('>H', V)[0]
+ elif T == Parameter.LTO:
+ if L != 1:
+ raise DecodeError("LTO TLV length error")
+ V = struct.unpack('B', V)[0]
+ elif T == Parameter.RW:
+ if L != 1:
+ raise DecodeError("RW TLV length error")
+ V = struct.unpack('B', V)[0]
+ if V & 0xF0:
+ log.warning("RW TLV reserved bits set")
+ V = V & 0x0F
+ elif T == Parameter.SN and L == 0:
+ log.warning("SN TLV with zero-length service name")
+ elif T == Parameter.OPT:
+ if L != 1:
+ raise DecodeError("OPT TLV length error")
+ V = struct.unpack_from('B', V)[0]
+ if V & 0xF8:
+ log.warning("OPT TLV reserved bits set")
+ V = V & 0x07
+ elif T == Parameter.SDREQ:
+ if L == 0:
+ raise DecodeError("SDREQ TLV length error")
+ if L == 1:
+ log.warning("SDREQ TLV with zero-length service name")
+ V = struct.unpack('B%ds' % (L-1), V)
+ elif T == Parameter.SDRES:
+ if L != 2:
+ raise DecodeError("SDRES TLV length error")
+ V = struct.unpack('BB', V)
+ elif T == Parameter.ECPK:
+ if L == 0:
+ log.warning("ECPK TLV with zero-length value")
+ if L & 1:
+ log.warning("ECPK TLV with odd length value")
+ elif T == Parameter.RN:
+ if L == 0:
+ log.warning("RN TLV with zero-length value")
+
+ return (T, L, V)
+
+ @staticmethod
+ def encode(T, V):
+ try:
+ if T in (Parameter.VERSION, Parameter.LTO,
+ Parameter.RW, Parameter.OPT):
+ return struct.pack('BBB', T, 1, V)
+ if T in (Parameter.MIUX, Parameter.WKS):
+ return struct.pack('>BBH', T, 2, V)
+ if T in (Parameter.SN, Parameter.ECPK, Parameter.RN):
+ if len(V) > 255:
+ raise EncodeError("can't encode TLV T=%d, V=%r" % (T, V))
+ return struct.pack('BB', T, len(V)) + bytes(V)
+ if T == Parameter.SDREQ:
+ tid, sn = V[0], V[1]
+ if len(sn) > 254:
+ raise EncodeError("can't encode TLV T=%d, V=%r" % (T, V))
+ return struct.pack('>BBB', T, 1+len(sn), tid) + bytes(sn)
+ if T == Parameter.SDRES:
+ tid, sap = V[0], V[1]
+ return struct.pack('>BBBB', T, 2, tid, sap)
+ raise EncodeError("unknown TLV T=%d, V=%r" % (T, V))
+ except struct.error as error:
+ msg = " for TLV T=%d, V=%r" % (T, V)
+ raise EncodeError(str(error) + msg)
+
+
+# -----------------------------------------------------------------------------
+# ProtocolDataUnit Base Class
+# -----------------------------------------------------------------------------
+class ProtocolDataUnit(object):
+ header_size = 2
+
+ def __init__(self, ptype, dsap, ssap):
+ self.ptype = ptype
+ self.dsap = dsap
+ self.ssap = ssap
+
+ @classmethod
+ def decode_header(cls, data, offset=0, size=None):
+ if size is None:
+ size = len(data) - offset
+ if size < cls.header_size:
+ raise DecodeError("insufficient pdu header bytes")
+ (dsap, ssap) = struct.unpack_from('!BB', data, offset)
+ return (dsap >> 2, ssap & 63)
+
+ def encode_header(self):
+ if self.dsap is None or self.ssap is None:
+ raise EncodeError("pdu dsap and ssap field can not be None")
+ if self.dsap < 0 or self.ssap < 0:
+ raise EncodeError("pdu dsap and ssap field can not be < 0")
+ if self.dsap > 63 or self.ssap > 63:
+ raise EncodeError("pdu dsap and ssap field can not be > 63")
+ return struct.pack('!H', self.dsap << 10 | self.ptype << 6 | self.ssap)
+
+ def __eq__(self, other):
+ return self.encode() == other.encode()
+
+ def __str__(self):
+ string = "{pdu.ssap:2} -> {pdu.dsap:2} {pdu.name:4.4s}"
+ return string.format(pdu=self)
+
+
+# -----------------------------------------------------------------------------
+# NumberedProtocolDataUnit Base Class
+# -----------------------------------------------------------------------------
+class NumberedProtocolDataUnit(ProtocolDataUnit):
+ header_size = 3
+
+ def __init__(self, ptype, dsap, ssap, ns, nr):
+ super(NumberedProtocolDataUnit, self).__init__(ptype, dsap, ssap)
+ self.ns, self.nr = ns, nr
+
+ @classmethod
+ def decode_header(cls, data, offset=0, size=None):
+ if size is None:
+ size = len(data) - offset
+ if size < cls.header_size:
+ raise DecodeError("numbered pdu header length error")
+ (dsap, ssap, sequence) = struct.unpack_from('!BBB', data, offset)
+ return (dsap >> 2, ssap & 63, sequence >> 4, sequence & 15)
+
+ def encode_header(self):
+ data = super(NumberedProtocolDataUnit, self).encode_header()
+ if self.ns is None or self.nr is None:
+ raise EncodeError("pdu ns and nr field can not be None")
+ if self.ns < 0 or self.nr < 0:
+ raise EncodeError("pdu ns and nr field can not be < 0")
+ if self.ns > 15 or self.nr > 15:
+ raise EncodeError("pdu ns and nr field can not be > 15")
+ return data + struct.pack('!B', self.ns << 4 | self.nr)
+
+ def __len__(self):
+ return 3
+
+ def __str__(self):
+ f = " N(R)={p.nr}" if self.ns is None else " N(S)={p.ns} N(R)={p.nr}"
+ return super(NumberedProtocolDataUnit, self).__str__()+f.format(p=self)
+
+
+# -----------------------------------------------------------------------------
+# Symmetry PDU
+# -----------------------------------------------------------------------------
+class Symmetry(ProtocolDataUnit):
+ name = "SYMM"
+
+ def __init__(self, dsap=0, ssap=0):
+ super(Symmetry, self).__init__(0b0000, dsap, ssap)
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap = cls.decode_header(data, offset, size)
+ if dsap != 0 or ssap != 0:
+ raise DecodeError("SSAP and DSAP must be 0 in SYMM PDU")
+ if size >= 3:
+ raise DecodeError("SYMM PDU PAYLOAD must be empty")
+ return Symmetry(dsap, ssap)
+
+ def encode(self):
+ if self.dsap != 0 or self.ssap != 0:
+ raise EncodeError("SSAP and DSAP must be 0 in SYMM PDU")
+ return self.encode_header()
+
+ def __len__(self):
+ return 2
+
+ def __str__(self):
+ return super(Symmetry, self).__str__()
+
+
+# -----------------------------------------------------------------------------
+# Parameter Exchange PDU
+# -----------------------------------------------------------------------------
+class ParameterExchange(ProtocolDataUnit):
+ name = "PAX"
+
+ def __init__(self, dsap=0, ssap=0, version=None, miux=None,
+ wks=None, lto=None, opt=None):
+ super(ParameterExchange, self).__init__(0b0001, dsap, ssap)
+ self._version = version
+ self._miux = miux
+ self._wks = wks
+ self._lto = lto
+ self._opt = opt
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap = cls.decode_header(data, offset, size)
+ if dsap != 0 or ssap != 0:
+ raise DecodeError("SSAP and DSAP must be 0 in PAX PDU")
+ pax_pdu = ParameterExchange(dsap, ssap)
+ offset, size = offset + 2, size - 2
+ while size >= 2:
+ T, L, V = Parameter.decode(data, offset)
+ if T == Parameter.VERSION:
+ pax_pdu._version = V
+ elif T == Parameter.MIUX:
+ pax_pdu._miux = V
+ elif T == Parameter.WKS:
+ pax_pdu._wks = V
+ elif T == Parameter.LTO:
+ pax_pdu._lto = V
+ elif T == Parameter.OPT:
+ pax_pdu._opt = V
+ else:
+ log.warning("invalid TLV %r in PAX PDU", (T, L, V))
+ offset, size = offset + 2 + L, size - 2 - L
+ return pax_pdu
+
+ def encode(self):
+ if self.dsap != 0 or self.ssap != 0:
+ raise EncodeError("SSAP and DSAP must be 0 in PAX PDU")
+ data = self.encode_header()
+ if self._version is not None:
+ data += Parameter.encode(Parameter.VERSION, self._version)
+ if self._miux is not None:
+ data += Parameter.encode(Parameter.MIUX, self._miux)
+ if self._wks is not None:
+ data += Parameter.encode(Parameter.WKS, self._wks)
+ if self._lto is not None:
+ data += Parameter.encode(Parameter.LTO, self._lto)
+ if self._opt is not None:
+ data += Parameter.encode(Parameter.OPT, self._opt)
+ return data
+
+ def __len__(self):
+ return (2 +
+ (3 if self._version is not None else 0) +
+ (4 if self._miux is not None else 0) +
+ (4 if self._wks is not None else 0) +
+ (3 if self._lto is not None else 0) +
+ (3 if self._opt is not None else 0))
+
+ @property
+ def version(self):
+ version = self._version
+ return (version >> 4, version & 15) if version else (0, 0)
+
+ @version.setter
+ def version(self, value):
+ self._version = (value[0] << 4 & 0xF0) | (value[1] & 0x0F)
+
+ @property
+ def version_text(self):
+ return "{0}.{1}".format(*self.version)
+
+ @property
+ def miu(self):
+ return self._miux + 128 if self._miux is not None else 128
+
+ @miu.setter
+ def miu(self, value):
+ self._miux = max(value - 128, 0)
+
+ @property
+ def wks(self):
+ return self._wks if self._wks is not None else 0
+
+ @wks.setter
+ def wks(self, value):
+ self._wks = value & 0xFFFF
+
+ @property
+ def wks_text(self):
+ t = {0: "LLC", 1: "SDP", 4: "SNEP"}
+ return ', '.join([
+ t.get(i, str(i)) for i in range(15, -1, -1) if self.wks >> i & 1])
+
+ @property
+ def lto(self):
+ return (self._lto if self._lto is not None else 10) * 10
+
+ @lto.setter
+ def lto(self, value):
+ self._lto = (value // 10) & 0xFF
+
+ @property
+ def lsc(self):
+ return self._opt & 3 if self._opt is not None else 0
+
+ @lsc.setter
+ def lsc(self, value):
+ self._opt = ((self._opt or 0) & 0b11111100) | (value & 0b00000011)
+
+ @property
+ def lsc_text(self):
+ return ("link service class unknown at activation",
+ "connection-less link service only",
+ "connection-oriented link service only",
+ "connection-less and connection-oriented")[self.lsc]
+
+ @property
+ def dpc(self):
+ return self._opt >> 2 & 1 if self._opt is not None else 0
+
+ @dpc.setter
+ def dpc(self, value):
+ self._opt = ((self._opt or 0) & 0b11111011) | (bool(value) << 2)
+
+ @property
+ def dpc_text(self):
+ return ("secure data transfer mode not supported",
+ "secure data transfer mode is supported")[self.dpc]
+
+ def __str__(self):
+ s = super(ParameterExchange, self).__str__()
+ if self._version is not None:
+ s += " VER={0}.{1}".format(*self.version)
+ if self._wks is not None:
+ s += " WKS={0:016b}".format(self._wks)
+ if self._miux is not None:
+ s += " MIUX={0}".format(self._miux)
+ if self._lto is not None:
+ s += " LTO={0}".format(self._lto)
+ if self._opt is not None:
+ s += " OPT={0:08b}".format(self._opt)
+ return s
+
+
+# -----------------------------------------------------------------------------
+# Aggregated Frame PDU
+# -----------------------------------------------------------------------------
+class AggregatedFrame(ProtocolDataUnit):
+ name = "AGF"
+
+ def __init__(self, dsap=0, ssap=0, aggregate=[]):
+ super(AggregatedFrame, self).__init__(0b0010, dsap, ssap)
+ self._aggregate = aggregate[:]
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap = cls.decode_header(data, offset, size)
+ if dsap != 0 or ssap != 0:
+ raise DecodeError("SSAP and DSAP must be 0 in AGF PDU")
+ agf_pdu = AggregatedFrame(dsap, ssap)
+ offset, size = offset + 2, size - 2
+ while size > 0:
+ try:
+ (pdu_size,) = struct.unpack_from('!H', data, offset)
+ except struct.error:
+ raise DecodeError("aggregated PDU length field error in AGF")
+ agf_pdu.append(decode(data, offset+2, pdu_size))
+ offset, size = offset + 2 + pdu_size, size - 2 - pdu_size
+ return agf_pdu
+
+ def encode(self):
+ if self.dsap != 0 or self.ssap != 0:
+ raise EncodeError("SSAP and DSAP must be 0 in AGF PDU")
+ data = self.encode_header()
+ for encoded_pdu in [pdu.encode() for pdu in self._aggregate]:
+ data += struct.pack('!H', len(encoded_pdu)) + encoded_pdu
+ return data
+
+ def append(self, pdu):
+ self._aggregate.append(pdu)
+
+ @property
+ def count(self):
+ return len(self._aggregate)
+
+ @property
+ def first(self):
+ return self._aggregate[0]
+
+ def __len__(self):
+ return 2 + sum([2+len(pdu) for pdu in self._aggregate])
+
+ def __str__(self):
+ def s(p):
+ return "LEN={0} '".format(len(p)) + \
+ ProtocolDataUnit.__str__(p).rstrip() + "'"
+ return super(AggregatedFrame, self).__str__() + \
+ " LEN={0} [".format(len(self)-2) + \
+ " ".join([s(p) for p in self._aggregate]) + "]"
+
+ def __iter__(self):
+ return AggregatedFrameIterator(self._aggregate)
+
+
+class AggregatedFrameIterator(object):
+ def __init__(self, aggregate):
+ self._aggregate = aggregate
+ self._current = 0
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ if self._current == len(self._aggregate):
+ raise StopIteration
+ self._current += 1
+ return self._aggregate[self._current-1]
+
+ def next(self):
+ return self.__next__()
+
+
+# -----------------------------------------------------------------------------
+# Unnumbered Information PDU
+# -----------------------------------------------------------------------------
+class UnnumberedInformation(ProtocolDataUnit):
+ name = "UI"
+
+ def __init__(self, dsap, ssap, data=None):
+ super(UnnumberedInformation, self).__init__(0b0011, dsap, ssap)
+ self.data = data if data else b''
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap = cls.decode_header(data, offset, size)
+ payload = bytes(data[offset+2:offset+size])
+ return UnnumberedInformation(dsap, ssap, payload)
+
+ def encode(self):
+ return self.encode_header() + bytes(self.data)
+
+ def __len__(self):
+ return 2 + len(self.data)
+
+ def __str__(self):
+ return super(UnnumberedInformation, self).__str__() + \
+ " LEN={0} DATA={1}".format(len(self.data), hexlify(self.data))
+
+
+# -----------------------------------------------------------------------------
+# Connect PDU
+# -----------------------------------------------------------------------------
+class Connect(ProtocolDataUnit):
+ name = "CONNECT"
+
+ def __init__(self, dsap, ssap, miu=128, rw=1, sn=None):
+ super(Connect, self).__init__(0b0100, dsap, ssap)
+ self.miu = miu
+ self.rw = rw
+ self.sn = sn
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap = cls.decode_header(data, offset, size)
+ connect_pdu = Connect(dsap, ssap)
+ offset, size = offset + 2, size - 2
+ while size >= 2:
+ T, L, V = Parameter.decode(data, offset)
+ if T == Parameter.MIUX:
+ connect_pdu.miu = 128 + V
+ elif T == Parameter.RW:
+ connect_pdu.rw = V
+ elif T == Parameter.SN:
+ connect_pdu.sn = V
+ else:
+ log.warning("invalid TLV %r in CONNECT PDU", (T, L, V))
+ offset, size = offset + 2 + L, size - 2 - L
+ return connect_pdu
+
+ def encode(self):
+ data = self.encode_header()
+ if self.miu and self.miu > 128:
+ data += Parameter.encode(Parameter.MIUX, self.miu - 128)
+ if self.rw and self.rw != 1:
+ data += Parameter.encode(Parameter.RW, self.rw)
+ if self.sn:
+ data += Parameter.encode(Parameter.SN, self.sn)
+ return data
+
+ def __len__(self):
+ return (2 +
+ (4 if self.miu and self.miu > 128 else 0) +
+ (3 if self.rw and self.rw != 1 else 0) +
+ (2 + len(self.sn) if self.sn else 0))
+
+ def __str__(self):
+ s = " MIU={conn.miu} RW={conn.rw} SN={conn.sn}"
+ return super(Connect, self).__str__() + s.format(conn=self)
+
+
+# -----------------------------------------------------------------------------
+# Disconnect PDU
+# -----------------------------------------------------------------------------
+class Disconnect(ProtocolDataUnit):
+ name = "DISC"
+
+ def __init__(self, dsap, ssap):
+ super(Disconnect, self).__init__(0b0101, dsap, ssap)
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap = cls.decode_header(data, offset, size)
+ return Disconnect(dsap, ssap)
+
+ def encode(self):
+ return self.encode_header()
+
+ def __len__(self):
+ return 2
+
+ def __str__(self):
+ return super(Disconnect, self).__str__()
+
+
+# -----------------------------------------------------------------------------
+# Connection Complete PDU
+# -----------------------------------------------------------------------------
+class ConnectionComplete(ProtocolDataUnit):
+ name = "CC"
+
+ def __init__(self, dsap, ssap, miu=128, rw=1):
+ super(ConnectionComplete, self).__init__(0b0110, dsap, ssap)
+ self.miu = miu
+ self.rw = rw
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap = cls.decode_header(data, offset, size)
+ cc_pdu = ConnectionComplete(dsap, ssap)
+ offset, size = offset + 2, size - 2
+ while size >= 2:
+ T, L, V = Parameter.decode(data, offset)
+ if T == Parameter.MIUX:
+ cc_pdu.miu = 128 + V
+ elif T == Parameter.RW:
+ cc_pdu.rw = V
+ else:
+ log.warning("invalid TLV %r in CC PDU", (T, L, V))
+ offset, size = offset + 2 + L, size - 2 - L
+ return cc_pdu
+
+ def encode(self):
+ data = self.encode_header()
+ if self.miu and self.miu > 128:
+ data += Parameter.encode(Parameter.MIUX, self.miu - 128)
+ if self.rw and self.rw != 1:
+ data += Parameter.encode(Parameter.RW, self.rw)
+ return data
+
+ def __len__(self):
+ return (2 +
+ (4 if self.miu and self.miu > 128 else 0) +
+ (3 if self.rw and self.rw != 1 else 0))
+
+ def __str__(self):
+ return super(ConnectionComplete, self).__str__() + \
+ " MIU={cc.miu} RW={cc.rw}".format(cc=self)
+
+
+# -----------------------------------------------------------------------------
+# Disconnected Mode PDU
+# -----------------------------------------------------------------------------
+class DisconnectedMode(ProtocolDataUnit):
+ name = "DM"
+
+ def __init__(self, dsap, ssap, reason=0):
+ super(DisconnectedMode, self).__init__(0b0111, dsap, ssap)
+ self.reason = reason
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ if size != 3:
+ raise DecodeError("DM PDU length error")
+ dsap, ssap = cls.decode_header(data, offset, size)
+ (reason,) = struct.unpack_from('!B', data, offset+2)
+ return DisconnectedMode(dsap, ssap, reason)
+
+ def encode(self):
+ return self.encode_header() + struct.pack('!B', self.reason)
+
+ def __len__(self):
+ return 3
+
+ def __str__(self):
+ return super(DisconnectedMode, self).__str__() + \
+ " REASON={dm.reason:02x}h".format(dm=self)
+
+ @property
+ def reason_text(self):
+ return {
+ 0x00: "disconnected",
+ 0x01: "inactive",
+ 0x02: "unbound",
+ 0x03: "rejected",
+ 0x10: "permanent reject for sap",
+ 0x11: "permanent reject for any",
+ 0x20: "temporary reject for sap",
+ 0x21: "temporary reject for any",
+ }.get(self.reason, "{0:02x}h".format(self.reason))
+
+
+# -----------------------------------------------------------------------------
+# Frame Reject PDU
+# -----------------------------------------------------------------------------
+class FrameReject(ProtocolDataUnit):
+ name = "FRMR"
+
+ def __init__(self, dsap, ssap, flags=0, ptype=0,
+ ns=0, nr=0, vs=0, vr=0, vsa=0, vra=0):
+ super(FrameReject, self).__init__(0b1000, dsap, ssap)
+ self.rej_flags = flags
+ self.rej_ptype = ptype
+ self.ns = ns
+ self.nr = nr
+ self.vs = vs
+ self.vr = vr
+ self.vsa = vsa
+ self.vra = vra
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ if size != 6:
+ raise DecodeError("FRMR PDU length error")
+ dsap, ssap = cls.decode_header(data, offset, size)
+ (b0, b1, b2, b3) = struct.unpack_from('!BBBB', data, offset+2)
+ flags, ptype = b0 >> 4, b0 & 15
+ ns, nr = b1 >> 4, b1 & 15
+ vs, vr = b2 >> 4, b2 & 15
+ vsa, vra = b3 >> 4, b3 & 15
+ return FrameReject(dsap, ssap, flags, ptype, ns, nr, vs, vr, vsa, vra)
+
+ @staticmethod
+ def from_pdu(pdu, flags, dlc):
+ rej_ptype = pdu.ptype
+ rej_flags = sum([1 << "SRIW".index(f) for f in flags])
+ frmr = FrameReject(pdu.ssap, pdu.dsap, rej_flags, rej_ptype)
+ if isinstance(pdu, Information):
+ frmr.ns, frmr.nr = pdu.ns, pdu.nr
+ if isinstance(pdu, ReceiveReady) or isinstance(pdu, ReceiveNotReady):
+ frmr.nr = pdu.nr
+ frmr.vs, frmr.vsa = dlc.send_cnt, dlc.send_ack
+ frmr.vr, frmr.vra = dlc.recv_cnt, dlc.recv_ack
+ return frmr
+
+ def encode(self):
+ return self.encode_header() + struct.pack(
+ '!BBBB', self.rej_flags << 4 | self.rej_ptype,
+ self.ns << 4 | self.nr, self.vs << 4 | self.vr,
+ self.vsa << 4 | self.vra)
+
+ def __len__(self):
+ return 6
+
+ def __str__(self):
+ return super(FrameReject, self).__str__() +\
+ " FLAGS={frmr.rej_flags:04b} PTYPE={frmr.rej_ptype:04b}"\
+ " N(S)={frmr.ns} N(R)={frmr.nr}"\
+ " V(S)={frmr.vs} V(R)={frmr.vr}"\
+ " V(SA)={frmr.vsa} V(RA)={frmr.vra}"\
+ .format(frmr=self)
+
+
+# -----------------------------------------------------------------------------
+# Service Name Lookup PDU
+# -----------------------------------------------------------------------------
+class ServiceNameLookup(ProtocolDataUnit):
+ name = "SNL"
+
+ def __init__(self, dsap, ssap, sdreq=None, sdres=None):
+ super(ServiceNameLookup, self).__init__(0b1001, dsap, ssap)
+ self.sdreq = sdreq if sdreq else list()
+ self.sdres = sdres if sdres else list()
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap = cls.decode_header(data, offset, size)
+ if dsap != 1 or ssap != 1:
+ raise DecodeError("SSAP and DSAP must be 1 in SNL PDU")
+ snl_pdu = ServiceNameLookup(dsap, ssap)
+ offset, size = offset + 2, size - 2
+ while size >= 2:
+ T, L, V = Parameter.decode(data, offset)
+ if T == Parameter.SDREQ:
+ snl_pdu.sdreq.append(V)
+ elif T == Parameter.SDRES:
+ snl_pdu.sdres.append(V)
+ else:
+ log.warning("invalid TLV %r in SNL PDU", (T, L, V))
+ offset, size = offset + 2 + L, size - 2 - L
+ return snl_pdu
+
+ def encode(self):
+ data = self.encode_header()
+ for sdreq in self.sdreq:
+ data += Parameter.encode(Parameter.SDREQ, sdreq)
+ for sdres in self.sdres:
+ data += Parameter.encode(Parameter.SDRES, sdres)
+ return data
+
+ def __len__(self):
+ return 2 + (len(self.sdres) * 4) \
+ + sum([3+len(sdreq[1]) for sdreq in self.sdreq])
+
+ def __str__(self):
+ return super(ServiceNameLookup, self).__str__() + \
+ " SDRES={0} SDREQ={1}".format(str(self.sdres), str(self.sdreq))
+
+
+# -----------------------------------------------------------------------------
+# Data Protection Setup PDU
+# -----------------------------------------------------------------------------
+class DataProtectionSetup(ProtocolDataUnit):
+ name = "DPS"
+
+ def __init__(self, dsap, ssap, ecpk=None, rn=None):
+ super(DataProtectionSetup, self).__init__(0b1010, dsap, ssap)
+ self.ecpk = ecpk
+ self.rn = rn
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap = cls.decode_header(data, offset, size)
+ if dsap != 0 or ssap != 0:
+ raise DecodeError("SSAP and DSAP must be 0 in DPS PDU")
+ dps_pdu = DataProtectionSetup(dsap, ssap)
+ offset, size = offset + 2, size - 2
+ while size >= 2:
+ T, L, V = Parameter.decode(data, offset)
+ if T == Parameter.ECPK:
+ dps_pdu.ecpk = V
+ elif T == Parameter.RN:
+ dps_pdu.rn = V
+ else:
+ log.debug("unknown TLV %r in DPS PDU", (T, L, V))
+ offset, size = offset + 2 + L, size - 2 - L
+ return dps_pdu
+
+ def encode(self):
+ if self.dsap != 0 or self.ssap != 0:
+ raise EncodeError("SSAP and DSAP must be 0 in DPS PDU")
+ data = self.encode_header()
+ if self.ecpk:
+ data += Parameter.encode(Parameter.ECPK, self.ecpk)
+ if self.rn:
+ data += Parameter.encode(Parameter.RN, self.rn)
+ return data
+
+ def __len__(self):
+ return (2 +
+ (2 + len(self.ecpk) if self.ecpk else 0) +
+ (2 + len(self.rn) if self.rn else 0))
+
+ def __str__(self):
+ return super(DataProtectionSetup, self).__str__() + \
+ " ECPK={0} RN={1}".format(
+ 'None' if self.ecpk is None else hexlify(self.ecpk).decode(),
+ 'None' if self.rn is None else hexlify(self.rn).decode())
+
+
+# -----------------------------------------------------------------------------
+# Information PDU
+# -----------------------------------------------------------------------------
+class Information(NumberedProtocolDataUnit):
+ name = "I"
+
+ def __init__(self, dsap, ssap, ns=None, nr=None, data=None):
+ super(Information, self).__init__(0b1100, dsap, ssap, ns, nr)
+ self.data = data if data else b''
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap, ns, nr = cls.decode_header(data, offset, size)
+ payload = bytes(data[offset+3:offset+size])
+ return cls(dsap, ssap, ns, nr, payload)
+
+ def encode(self):
+ return self.encode_header() + bytes(self.data)
+
+ def __len__(self):
+ return 3 + len(self.data)
+
+ def __str__(self):
+ return (super(Information, self).__str__() + " LEN={0} DATA={1}"
+ .format(len(self.data), hexlify(self.data)))
+
+
+# -----------------------------------------------------------------------------
+# Receive Ready PDU
+# -----------------------------------------------------------------------------
+class ReceiveReady(NumberedProtocolDataUnit):
+ name = "RR"
+
+ def __init__(self, dsap, ssap, nr=None):
+ super(ReceiveReady, self).__init__(0b1101, dsap, ssap, 0, nr)
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap, ns, nr = cls.decode_header(data, offset, size)
+ if ns != 0:
+ log.warning("reserved bits set in sequence field")
+ return cls(dsap, ssap, nr)
+
+ def encode(self):
+ return self.encode_header()
+
+
+# -----------------------------------------------------------------------------
+# Receive Not Ready PDU
+# -----------------------------------------------------------------------------
+class ReceiveNotReady(NumberedProtocolDataUnit):
+ name = "RNR"
+
+ def __init__(self, dsap, ssap, nr):
+ super(ReceiveNotReady, self).__init__(0b1110, dsap, ssap, 0, nr)
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap, ns, nr = cls.decode_header(data, offset, size)
+ if ns != 0:
+ log.warning("reserved bits set in sequence field")
+ return cls(dsap, ssap, nr)
+
+ def encode(self):
+ return self.encode_header()
+
+
+# -----------------------------------------------------------------------------
+# UnknownProtocolDataUnit
+# -----------------------------------------------------------------------------
+class UnknownProtocolDataUnit(ProtocolDataUnit):
+ def __init__(self, ptype, dsap, ssap, payload):
+ super(UnknownProtocolDataUnit, self).__init__(ptype, dsap, ssap)
+ self.name = "{0:04b}".format(ptype)
+ self.payload = payload
+
+ @classmethod
+ def decode(cls, data, offset, size):
+ dsap, ssap = cls.decode_header(data, offset, size)
+ pdutype = (data[offset] << 2 | data[offset+1] >> 6) & 0x0F
+ payload = data[offset+2:offset+size]
+ return cls(pdutype, dsap, ssap, payload)
+
+ def encode(self):
+ return self.encode_header() + bytes(self.payload)
+
+ def __len__(self):
+ return 2 + len(self.payload)
+
+ def __str__(self):
+ return (super(UnknownProtocolDataUnit, self).__str__()
+ + " PAYLOAD={}".format(hexlify(self.payload).decode()))
+
+
+# -----------------------------------------------------------------------------
+# pdu decode and encode functions
+# -----------------------------------------------------------------------------
+pdu_type_map = {
+ 0b0000: Symmetry,
+ 0b0001: ParameterExchange,
+ 0b0010: AggregatedFrame,
+ 0b0011: UnnumberedInformation,
+ 0b0100: Connect,
+ 0b0101: Disconnect,
+ 0b0110: ConnectionComplete,
+ 0b0111: DisconnectedMode,
+ 0b1000: FrameReject,
+ 0b1001: ServiceNameLookup,
+ 0b1010: DataProtectionSetup,
+ 0b1100: Information,
+ 0b1101: ReceiveReady,
+ 0b1110: ReceiveNotReady,
+}
+
+
+def decode(data, offset=0, size=None):
+ size = len(data) if size is None else size
+
+ if offset + size > len(data):
+ raise DecodeError("size bytes from offset exceed the data length")
+ if size < 2:
+ raise DecodeError("less than two header bytes can't make a valid pdu")
+
+ ptype = (struct.unpack_from('>H', data, offset)[0] >> 6) & 0b1111
+ pdu_type = pdu_type_map.get(ptype, UnknownProtocolDataUnit)
+ return pdu_type.decode(data, offset, size)
+
+
+def encode(pdu):
+ if not isinstance(pdu, ProtocolDataUnit):
+ raise AttributeError("can't encode %s" % type(pdu))
+
+ return pdu.encode()
diff --git a/src/lib/nfc/llcp/sec.py b/src/lib/nfc/llcp/sec.py
new file mode 100644
index 0000000..7fb6bc4
--- /dev/null
+++ b/src/lib/nfc/llcp/sec.py
@@ -0,0 +1,542 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import struct
+import ctypes
+import ctypes.util
+from ctypes import c_void_p, c_int
+from binascii import hexlify
+
+import logging
+log = logging.getLogger(__name__)
+
+OpenSSL = None
+
+
+class Error(Exception):
+ pass
+
+
+class EncryptionError(Error):
+ pass
+
+
+class DecryptionError(Error):
+ pass
+
+
+class KeyAgreementError(Error):
+ pass
+
+
+def cipher_suite(name):
+ if name == "ECDH_anon_WITH_AEAD_AES_128_CCM_4":
+ return CipherSuite1()
+
+
+class CipherSuite1:
+ _ccm_t = 4
+ _ccm_q = 2
+ _ccm_n = 13
+
+ def __init__(self):
+ self.random_nonce = None
+ self.public_key_x = None
+ self.public_key_y = None
+ ec_key = OpenSSL.EC_KEY.new_by_curve_name(OpenSSL.NID_X9_62_prime256v1)
+ if ec_key and ec_key.generate_key() and ec_key.check_key():
+ pubkey = ec_key.get_public_key()
+ x, y = pubkey.get_affine_coordinates_GFp(ec_key.get_group())
+ self.public_key_x = x
+ self.public_key_y = y
+ self.random_nonce = OpenSSL.rand_bytes(8)
+ self._ec_key = ec_key
+
+ def calculate_session_key(self, ecpk, rn_i=None, rn_t=None):
+ if ecpk is None:
+ raise KeyAgreementError("remote public key is required")
+ if len(ecpk) != 64:
+ raise KeyAgreementError("remote public key has wrong size")
+ if rn_i is None and rn_t is None:
+ raise KeyAgreementError("remote random nonce is required")
+ if rn_i and len(rn_i) != 8:
+ raise KeyAgreementError("initiator random nonce has wrong size")
+ if rn_t and len(rn_t) != 8:
+ raise KeyAgreementError("target random nonce has wrong size")
+
+ if rn_i is None:
+ rn_i = self.random_nonce
+ if rn_t is None:
+ rn_t = self.random_nonce
+
+ ec_key = OpenSSL.EC_KEY.new_by_curve_name(OpenSSL.NID_X9_62_prime256v1)
+ try:
+ ec_key.set_public_key_affine_coordinates(ecpk[:32], ecpk[32:])
+ except AssertionError:
+ raise KeyAgreementError("remote public key is not on curve")
+
+ cipher = OpenSSL.EVP_aes_128_cbc()
+ secret = OpenSSL.ECDH(self._ec_key) \
+ .compute_key(ec_key.get_public_key())
+ k_encr = OpenSSL.CMAC(cipher) \
+ .init(rn_i+rn_t) \
+ .update(secret).final()
+
+ log.debug("remote ecpk-x %r", hexlify(ecpk[:32]))
+ log.debug("remote ecpk-y %r", hexlify(ecpk[32:]))
+ log.debug("shared secret %r", hexlify(secret))
+ log.debug("shared nonce %r", hexlify(rn_i+rn_t))
+ log.debug("session key %r", hexlify(k_encr))
+
+ self._pcs = self._pcr = 0
+ self._k_encr = k_encr
+ return self._k_encr
+
+ @property
+ def icv_size(self):
+ return self._ccm_t
+
+ def encrypt(self, a, p):
+ # The nonce N is a leftmost 40-bit fixed part all bits zero
+ # and a rightmost 64-bit counter part taken from PC(S).
+ nonce = struct.pack('!xxxxxQ', self._pcs)
+ if self._pcs < 0xFFFFFFFFFFFFFFFF:
+ self._pcs += 1
+ else:
+ raise EncryptionError("send counter out of range")
+
+ # The encryption key was computed in calculate_session_key()
+ key = self._k_encr
+
+ # OpenSSLWrapper methods raise AssertionError when any of the
+ # operations failed.
+ try:
+ return self._encrypt(bytes(a), bytes(p), key, nonce, self._ccm_t)
+ except AssertionError:
+ error = "encrypt failed for message %d" % self._pcs
+ log.error(error)
+ raise EncryptionError(error)
+
+ @staticmethod
+ def _encrypt(aad, txt, key, nonce, tlen):
+ # from https://wiki.openssl.org/index.php/
+ # EVP_Authenticated_Encryption_and_Decryption#
+ # Authenticated_Encryption_using_CCM_mode
+ evp = OpenSSL.EVP()
+ evp.encrypt_init(OpenSSL.EVP_aes_128_ccm())
+ evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_IVLEN, len(nonce))
+ evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_TAG, tlen)
+ evp.encrypt_init(key=key, iv=nonce)
+ evp.encrypt_update(None, None, len(txt))
+ evp.encrypt_update(None, aad, len(aad))
+ return evp.encrypt_update(len(txt), txt, len(txt)) + \
+ evp.cipher_ctx.ctrl_get(OpenSSL.EVP.CTRL_CCM_GET_TAG, tlen)
+
+ def decrypt(self, a, c):
+ # The nonce N is a leftmost 40-bit fixed part all bits zero
+ # and a rightmost 64-bit counter part taken from PC(R).
+ nonce = struct.pack('!xxxxxQ', self._pcr)
+ if self._pcr < 0xFFFFFFFFFFFFFFFF:
+ self._pcr += 1
+ else:
+ raise DecryptionError("recv counter out of range")
+
+ # The decryption key was computed in calculate_session_key()
+ key = self._k_encr
+
+ # OpenSSLWrapper methods raise AssertionError when any of the
+ # operations failed.
+ try:
+ return self._decrypt(bytes(a), bytes(c), key, nonce, self._ccm_t)
+ except AssertionError:
+ error = "decrypt failed for message %d" % self._pcr
+ log.error(error)
+ raise DecryptionError(error)
+
+ @staticmethod
+ def _decrypt(aad, txt, key, nonce, tlen):
+ # from https://wiki.openssl.org/index.php/
+ # EVP_Authenticated_Encryption_and_Decryption#
+ # Authenticated_Decryption_using_CCM_mode
+ tag = txt[-tlen:]
+ txt = txt[:-tlen]
+ evp = OpenSSL.EVP()
+ evp.decrypt_init(OpenSSL.EVP_aes_128_ccm())
+ evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_IVLEN, len(nonce))
+ evp.cipher_ctx.ctrl_set(OpenSSL.EVP.CTRL_CCM_SET_TAG, len(tag), tag)
+ evp.decrypt_init(key=key, iv=nonce)
+ evp.decrypt_update(None, None, len(txt))
+ evp.decrypt_update(None, aad, len(aad))
+ return evp.decrypt_update(len(txt), txt, len(txt))
+
+
+class OpenSSLWrapper:
+ NID_X9_62_prime256v1 = 415 # NIST Curve P-256
+
+ def __init__(self, libcrypto):
+ self.crypto = ctypes.CDLL(libcrypto)
+ self.crypto.BN_new.restype = c_void_p
+ self.crypto.BN_num_bits.restype = c_int
+ self.crypto.BN_bn2bin.restype = c_int
+ self.crypto.BN_bin2bn.restype = c_void_p
+ self.crypto.BN_free.restype = None
+ self.crypto.RAND_bytes.restype = c_int
+ self.crypto.EC_KEY_new_by_curve_name.restype = c_void_p
+ self.crypto.EC_KEY_generate_key.restype = c_int
+ self.crypto.EC_KEY_check_key.restype = c_int
+ self.crypto.EC_KEY_set_public_key.restype = c_int
+ self.crypto.EC_KEY_set_public_key_affine_coordinates.restype = c_int
+ self.crypto.EC_KEY_get0_public_key.restype = c_void_p
+ self.crypto.EC_KEY_get0_group.restype = c_void_p
+ self.crypto.EC_KEY_free.restype = None
+ self.crypto.EC_POINT_new.restype = c_void_p
+ self.crypto.EC_POINT_get_affine_coordinates_GFp.restype = c_int
+ self.crypto.EC_POINT_set_affine_coordinates_GFp.restype = c_int
+ self.crypto.EC_POINT_free.restype = None
+ self.crypto.ECDH_OpenSSL.restype = c_void_p
+ self.crypto.ECDH_set_method.restype = c_int
+ self.crypto.ECDH_compute_key.restype = c_int
+ self.crypto.CMAC_CTX_new.restype = c_void_p
+ self.crypto.CMAC_CTX_free.restype = None
+ self.crypto.CMAC_Init.restype = c_int
+ self.crypto.CMAC_Update.restype = c_int
+ self.crypto.CMAC_Final.restype = c_int
+
+ self.crypto.EVP_CIPHER_CTX_new.restype = c_void_p
+ self.crypto.EVP_CIPHER_CTX_init.restype = None
+ self.crypto.EVP_CIPHER_CTX_ctrl.restype = c_int
+ self.crypto.EVP_CIPHER_CTX_free.restype = None
+
+ self.crypto.EVP_EncryptInit_ex.restype = c_int
+ self.crypto.EVP_EncryptUpdate.restype = c_int
+ self.crypto.EVP_EncryptFinal.restype = c_int
+ self.crypto.EVP_DecryptInit_ex.restype = c_int
+ self.crypto.EVP_DecryptUpdate.restype = c_int
+ self.crypto.EVP_DecryptFinal.restype = c_int
+
+ self.crypto.EVP_aes_128_cbc.restype = c_void_p
+ self.crypto.EVP_aes_128_cbc.argtypes = []
+ self.crypto.EVP_aes_128_ccm.restype = c_void_p
+ self.crypto.EVP_aes_128_ccm.argtypes = []
+
+ self.EVP_aes_128_cbc = self.crypto.EVP_aes_128_cbc
+ self.EVP_aes_128_ccm = self.crypto.EVP_aes_128_ccm
+
+ class BIGNUM:
+ def __init__(self, bignum, release=False):
+ self._bignum = bignum
+ self._release = release
+
+ def __del__(self):
+ if self._release:
+ OpenSSL.crypto.BN_free(self)
+
+ @property
+ def _as_parameter_(self):
+ return c_void_p(self._bignum)
+
+ @staticmethod
+ def new():
+ # BIGNUM *BN_new(void);
+ bignum = OpenSSL.crypto.BN_new()
+ if bignum is None:
+ log.error("BN_new")
+ else:
+ return OpenSSL.BIGNUM(bignum, release=True)
+
+ def num_bits(self):
+ return OpenSSL.crypto.BN_num_bits(self)
+
+ def num_bytes(self):
+ return (self.num_bits() + 7) // 8
+
+ def bn2bin(self, num_bytes=None):
+ # int BN_bn2bin(const BIGNUM *a, unsigned char *to);
+ if num_bytes is None:
+ num_bytes = self.num_bytes()
+ else:
+ assert num_bytes >= self.num_bytes(), "bn2bin num bytes"
+ strbuf = ctypes.create_string_buffer(num_bytes)
+ OpenSSL.crypto.BN_bn2bin(self, strbuf)
+ return strbuf.raw
+
+ @staticmethod
+ def bin2bn(s):
+ # BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret);
+ strbuf = ctypes.create_string_buffer(bytes(s), len(s))
+ res = OpenSSL.crypto.BN_bin2bn(strbuf, len(s), None)
+ if res is None:
+ log.error("BN_bin2bn")
+ else:
+ return OpenSSL.BIGNUM(res)
+
+ def rand_bytes(self, num):
+ # int RAND_bytes(unsigned char *buf, int num);
+ buf = ctypes.create_string_buffer(num)
+ res = self.crypto.RAND_bytes(buf, c_int(num))
+ if res == 0:
+ log.error("RAND_bytes")
+ else:
+ return buf.raw
+
+ class EC_KEY:
+ def __init__(self, ec_key):
+ self._ec_key = ec_key
+
+ def __del__(self):
+ OpenSSL.crypto.EC_KEY_free(self)
+
+ @property
+ def _as_parameter_(self):
+ return c_void_p(self._ec_key)
+
+ @staticmethod
+ def new_by_curve_name(nid):
+ # EC_KEY *EC_KEY_new_by_curve_name(int nid);
+ res = OpenSSL.crypto.EC_KEY_new_by_curve_name(c_int(nid))
+ if res is None:
+ log.error("EC_KEY_new_by_curve_name")
+ else:
+ return OpenSSL.EC_KEY(res)
+
+ def generate_key(self):
+ # int EC_KEY_generate_key(EC_KEY *key);
+ res = OpenSSL.crypto.EC_KEY_generate_key(self)
+ if res == 0:
+ log.error("EC_KEY_generate_key")
+ return bool(res)
+
+ def check_key(self):
+ # int EC_KEY_check_key(const EC_KEY *key);
+ res = OpenSSL.crypto.EC_KEY_check_key(self)
+ if res == 0:
+ log.error("EC_KEY_check_key")
+ return bool(res)
+
+ def set_public_key_affine_coordinates(self, pubkey_x, pubkey_y):
+ # int EC_KEY_set_public_key_affine_coordinates(EC_KEY *key,
+ # BIGNUM *x, BIGNUM *y);
+ r = OpenSSL.crypto.EC_KEY_set_public_key_affine_coordinates(
+ self, *list(map(OpenSSL.BIGNUM.bin2bn, (pubkey_x, pubkey_y))))
+ if r != 1:
+ errmsg = "EC_KEY_set_public_key_affine_coordinates"
+ raise AssertionError(errmsg)
+
+ def get_public_key(self):
+ # const EC_POINT *EC_KEY_get0_public_key(const EC_KEY *key);
+ res = OpenSSL.crypto.EC_KEY_get0_public_key(self)
+ if res is None:
+ log.error("EC_KEY_get0_public_key")
+ else:
+ return OpenSSL.EC_POINT(res)
+
+ def get_group(self):
+ # const EC_GROUP *EC_KEY_get0_group(const EC_KEY *key);
+ res = OpenSSL.crypto.EC_KEY_get0_group(self)
+ if res is None:
+ log.error("EC_KEY_get0_group")
+ else:
+ return OpenSSL.EC_GROUP(res)
+
+ class EC_GROUP:
+ def __init__(self, ec_group):
+ self._ec_group = ec_group
+
+ @property
+ def _as_parameter_(self):
+ return c_void_p(self._ec_group)
+
+ class EC_POINT:
+ def __init__(self, ec_point):
+ self._ec_point = ec_point
+
+ @property
+ def _as_parameter_(self):
+ return c_void_p(self._ec_point)
+
+ def get_affine_coordinates_GFp(self, ec_group):
+ # int EC_POINT_get_affine_coordinates_GFp(const EC_GROUP *group,
+ # const EC_POINT *p, BIGNUM *x, BIGNUM *y, BN_CTX *ctx);
+ x, y = (OpenSSL.BIGNUM.new(), OpenSSL.BIGNUM.new())
+ func = OpenSSL.crypto.EC_POINT_get_affine_coordinates_GFp
+ res = func(ec_group, self, x, y, None)
+ if res == 0:
+ log.error("EC_POINT_get_affine_coordinates_GFp")
+ else:
+ return (x.bn2bin(32), y.bn2bin(32))
+
+ class ECDH:
+ def __init__(self, local_key):
+ self.key = local_key
+ method = OpenSSL.crypto.ECDH_OpenSSL()
+ OpenSSL.crypto.ECDH_set_method(self.key, c_void_p(method))
+
+ def compute_key(self, pub_key):
+ # int ECDH_compute_key(void *out, size_t outlen,
+ # const EC_POINT *pub_key, EC_KEY *ecdh,
+ # void *(*KDF)(const void *in, size_t inlen,
+ # void *out, size_t *outlen));
+ strbuf = ctypes.create_string_buffer(32)
+ args = (strbuf, 32, pub_key, self.key, None)
+ r = OpenSSL.crypto.ECDH_compute_key(*args)
+ assert r == 32, "ECDH_compute_key"
+ return strbuf.raw # the shared secret z
+
+ class CMAC:
+ def __init__(self, cipher):
+ # CMAC_CTX *CMAC_CTX_new(void);
+ self._cmac_ctx = OpenSSL.crypto.CMAC_CTX_new()
+ self._cipher = cipher
+
+ def __del__(self):
+ # void CMAC_CTX_free(CMAC_CTX *ctx);
+ OpenSSL.crypto.CMAC_CTX_free(self)
+
+ @property
+ def _as_parameter_(self):
+ return c_void_p(self._cmac_ctx)
+
+ def init(self, key):
+ # int CMAC_Init(CMAC_CTX *ctx, const void *key, size_t keylen,
+ # const EVP_CIPHER *cipher, ENGINE *impl);
+ assert len(key) == 16
+ keybuf = ctypes.create_string_buffer(key, 16)
+ keylen = ctypes.c_size_t(16)
+ cipher = ctypes.c_void_p(self._cipher)
+ r = OpenSSL.crypto.CMAC_Init(self, keybuf, keylen, cipher, None)
+ assert r == 1, "CMAC_Init"
+ return self
+
+ def update(self, msg):
+ # int CMAC_Update(CMAC_CTX *ctx, const void *data, size_t dlen);
+ msgbuf = ctypes.create_string_buffer(msg, len(msg))
+ msglen = ctypes.c_size_t(len(msg))
+ r = OpenSSL.crypto.CMAC_Update(self, msgbuf, msglen)
+ assert r == 1, "CMAC_Update"
+ return self
+
+ def final(self):
+ macbuf = ctypes.create_string_buffer(16)
+ maclen = ctypes.c_size_t(0)
+ rc = OpenSSL.crypto.CMAC_Final(self, macbuf, ctypes.byref(maclen))
+ assert rc == 1 and maclen.value == 16, "CMAC_Final"
+ return macbuf.raw
+
+ class EVP:
+ CTRL_CCM_SET_IVLEN = 0x09
+ CTRL_CCM_GET_TAG = 0x10
+ CTRL_CCM_SET_TAG = 0x11
+ CTRL_CCM_SET_L = 0x14
+
+ class CIPHER_CTX:
+ def __init__(self):
+ # EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void);
+ ctx = OpenSSL.crypto.EVP_CIPHER_CTX_new()
+ if ctx is None:
+ raise AssertionError("EVP_CIPHER_CTX_new")
+ self._ctx = ctx
+
+ def __del__(self):
+ # void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *ctx);
+ OpenSSL.crypto.EVP_CIPHER_CTX_free(self)
+
+ @property
+ def _as_parameter_(self):
+ return c_void_p(self._ctx)
+
+ def ctrl_set(self, op, arg, ptr=None):
+ # int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type,
+ # int arg, void *ptr);
+ r = OpenSSL.crypto.EVP_CIPHER_CTX_ctrl(self, op, arg, ptr)
+ if r != 1:
+ raise AssertionError("EVP_CIPHER_CTX_ctrl")
+
+ def ctrl_get(self, op, arg):
+ # int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type,
+ # int arg, void *ptr);
+ outbuf = ctypes.create_string_buffer(arg)
+ r = OpenSSL.crypto.EVP_CIPHER_CTX_ctrl(self, op, arg, outbuf)
+ if r != 1:
+ raise AssertionError("EVP_CIPHER_CTX_ctrl")
+ return outbuf.raw
+
+ def __init__(self, evp_cipher_ctx=None):
+ if evp_cipher_ctx:
+ self._ctx = evp_cipher_ctx
+ else:
+ self._ctx = OpenSSL.EVP.CIPHER_CTX()
+
+ @property
+ def cipher_ctx(self):
+ return self._ctx
+
+ def encrypt_init(self, evp_cipher=None, key=None, iv=None):
+ # int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx,
+ # const EVP_CIPHER *type, ENGINE *impl,
+ # unsigned char *key, unsigned char *iv);
+ r = OpenSSL.crypto.EVP_EncryptInit_ex(
+ self._ctx, c_void_p(evp_cipher), None, key, iv)
+ if r != 1:
+ raise AssertionError("EVP_EncryptInit_ex")
+
+ def encrypt_update(self, out_len, message, msg_len):
+ # int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
+ # int *outl, unsigned char *in, int inl);
+ if out_len is None:
+ out_buf = None
+ out_len = c_int(0)
+ else:
+ out_buf = ctypes.create_string_buffer(out_len)
+ out_len = c_int(out_len)
+ r = OpenSSL.crypto.EVP_EncryptUpdate(
+ self._ctx, out_buf, ctypes.byref(out_len), message, msg_len)
+ if r != 1:
+ raise AssertionError("EVP_EncryptUpdate")
+ return out_buf.raw[0:out_len.value] if out_buf else b''
+
+ def decrypt_init(self, evp_cipher=None, key=None, iv=None):
+ # int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx,
+ # const EVP_CIPHER *type, ENGINE *impl,
+ # unsigned char *key, unsigned char *iv);
+ r = OpenSSL.crypto.EVP_DecryptInit_ex(
+ self._ctx, c_void_p(evp_cipher), None, key, iv)
+ if r != 1:
+ raise AssertionError("EVP_DecryptInit_ex")
+
+ def decrypt_update(self, out_len, message, msg_len):
+ # int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
+ # int *outl, unsigned char *in, int inl);
+ if out_len is None:
+ out_buf = None
+ out_len = c_int(0)
+ else:
+ out_buf = ctypes.create_string_buffer(out_len)
+ out_len = c_int(out_len)
+ r = OpenSSL.crypto.EVP_DecryptUpdate(
+ self._ctx, out_buf, ctypes.byref(out_len), message, msg_len)
+ if r != 1:
+ raise AssertionError("EVP_DecryptUpdate")
+ return out_buf.raw[0:out_len.value] if out_buf else b''
+
+
+libcrypto = ctypes.util.find_library('crypto.so.1.0')
+if libcrypto is not None:
+ OpenSSL = OpenSSLWrapper(libcrypto)
diff --git a/src/lib/nfc/llcp/socket.py b/src/lib/nfc/llcp/socket.py
new file mode 100644
index 0000000..a51d915
--- /dev/null
+++ b/src/lib/nfc/llcp/socket.py
@@ -0,0 +1,177 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+
+
+class Socket(object):
+ """
+ Create a new LLCP socket with the given socket type. The
+ socket type should be one of:
+
+ * :const:`nfc.llcp.LOGICAL_DATA_LINK` for best-effort
+ communication using LLCP connection-less PDU exchange
+
+ * :const:`nfc.llcp.DATA_LINK_CONNECTION` for reliable
+ communication using LLCP connection-mode PDU exchange
+
+ * :const:`nfc.llcp.llc.RAW_ACCESS_POINT` for unregulated LLCP PDU
+ exchange (useful to implement test programs)
+ """
+ def __init__(self, llc, sock_type):
+ self._tco = None if sock_type is None else llc.socket(sock_type)
+ self._llc = llc
+
+ @property
+ def llc(self):
+ """The :class:`~nfc.llcp..llc.LogicalLinkController` instance
+ to which this socket belongs. This attribute is read-only."""
+ return self._llc
+
+ def resolve(self, name):
+ """Resolve a service name into an address. This may involve
+ conversation with the remote service discovery component if
+ the name is hasn't yet been resolved. The return value is the
+ service access point address for the service name bound at the
+ remote device. The address value 0 indicates that the remote
+ device does not have a service with the requested name. The
+ address value 1 indicates that the remote device has a data
+ link connection service with the requested name that can only
+ be connected by service name. The return value is None when
+ communication with the peer device terminated while waiting
+ for a response.
+
+ """
+ return self.llc.resolve(name)
+
+ def setsockopt(self, option, value):
+ """Set the value of the given socket option and return the
+ current value which may have been corrected if it was out of
+ bounds."""
+ return self.llc.setsockopt(self._tco, option, value)
+
+ def getsockopt(self, option):
+ """Return the value of the given socket option."""
+ return self.llc.getsockopt(self._tco, option)
+
+ def bind(self, address=None):
+ """Bind the socket to address. The socket must not already be
+ bound. The address may be a service name string, a service
+ access point number, or it may be omitted. If address is a
+ well-known service name the socket will be bound to the
+ corresponding service access point address, otherwise the
+ socket will be bound to the next available service access
+ point address between 16 and 31 (inclusively). If address is a
+ number between 32 and 63 (inclusively) the socket will be
+ bound to that service access point address. If the address
+ argument is omitted the socket will be bound to the next
+ available service access point address between 32 and 63."""
+ return self.llc.bind(self._tco, address)
+
+ def connect(self, address):
+ """Connect to a remote socket at address. Address may be a
+ service name string or a service access point number."""
+ return self.llc.connect(self._tco, address)
+
+ def listen(self, backlog):
+ """Mark a socket as a socket that will be used to accept
+ incoming connection requests using accept(). The *backlog*
+ defines the maximum length to which the queue of pending
+ connections for the socket may grow. A backlog of zero
+ disables queuing of connection requests.
+ """
+ return self.llc.listen(self._tco, backlog)
+
+ def accept(self):
+ """Accept a connection. The socket must be bound to an address
+ and listening for connections. The return value is a new
+ socket object usable to send and receive data on the
+ connection."""
+ socket = Socket(self._llc, None)
+ socket._tco = self.llc.accept(self._tco)
+ return socket
+
+ def send(self, data, flags=0):
+ """Send data to the socket. The socket must be connected to a remote
+ socket. Returns a boolean value that indicates success or
+ failure. A false value is typically an indication that the
+ socket or connection was closed.
+
+ """
+ return self.llc.send(self._tco, data, flags)
+
+ def sendto(self, data, addr, flags=0):
+ """Send data to the socket. The socket should not be connected
+ to a remote socket, since the destination socket is specified
+ by addr. Returns a boolean value that indicates success
+ or failure. Failure to send is generally an indication that
+ the socket was closed."""
+ return self.llc.sendto(self._tco, data, addr, flags)
+
+ def recv(self):
+ """Receive data from the socket. The return value is a bytes object
+ representing the data received. The maximum amount of data
+ that may be returned is determined by the link or connection
+ maximum information unit size."""
+ return self.llc.recv(self._tco)
+
+ def recvfrom(self):
+ """Receive data from the socket. The return value is a pair
+ (bytes, address) where string is a string representing the
+ data received and address is the address of the socket sending
+ the data."""
+ return self.llc.recvfrom(self._tco)
+
+ def poll(self, event, timeout=None):
+ """Wait for a socket event. Posssible *event* values are the strings
+ "recv", "send" and "acks". Whent the timeout is present and
+ not :const:`None`, it should be a floating point number
+ specifying the timeout for the operation in seconds (or
+ fractions thereof). For "recv" or "send" the :meth:`poll`
+ method returns :const:`True` if a next :meth:`recv` or
+ :meth:`send` operation would be non-blocking. The "acks" event
+ may only be used with a data-link-connection type socket; the
+ call then returns :const:`True` if the counter of received
+ acknowledgements was greater than zero and decrements the
+ counter by one.
+
+ """
+ return self.llc.poll(self._tco, event, timeout)
+
+ def getsockname(self):
+ """Obtain the address to which the socket is bound. For an
+ unbound socket the returned value is None.
+ """
+ return self.llc.getsockname(self._tco)
+
+ def getpeername(self):
+ """Obtain the address of the peer connected on the socket. For
+ an unconnected socket the returned value is None.
+ """
+ return self.llc.getpeername(self._tco)
+
+ def close(self):
+ """Close the socket. All future operations on the socket
+ object will fail. The remote end will receive no more data
+ Sockets are automatically closed when the logical link
+ controller terminates (gracefully or by link disruption). A
+ connection-mode socket will attempt to disconnect the data
+ link connection (if in connected state)."""
+ return self.llc.close(self._tco)
diff --git a/src/lib/nfc/llcp/tco.py b/src/lib/nfc/llcp/tco.py
new file mode 100644
index 0000000..3c049eb
--- /dev/null
+++ b/src/lib/nfc/llcp/tco.py
@@ -0,0 +1,733 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+from . import pdu
+from . import err
+import src.lib.nfc.llcp
+
+import errno
+import threading
+import collections
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class TransmissionControlObject(object):
+ class State(object):
+ def __init__(self):
+ self.names = ("SHUTDOWN", "CLOSED", "LISTEN", "CONNECT",
+ "ESTABLISHED", "DISCONNECT", "CLOSE_WAIT")
+ self.value = self.names.index("SHUTDOWN")
+
+ def __str__(self):
+ return self.names[self.value]
+
+ def __getattr__(self, name):
+ return self.value == self.names.index(name)
+
+ def __setattr__(self, name, value):
+ if name not in ("names", "value"):
+ value, name = self.names.index(name), "value"
+ object.__setattr__(self, name, value)
+
+ class Mode(object):
+ def __init__(self):
+ self.names = ("BLOCK", "SEND_BUSY", "RECV_BUSY", "RECV_BUSY_SENT")
+ self.value = dict([(name, False) for name in self.names])
+
+ def __str__(self):
+ return str(self.value)
+
+ def __getattr__(self, name):
+ return self.value[name]
+
+ def __init__(self, send_miu, recv_miu):
+ self.lock = threading.RLock()
+ self.mode = TransmissionControlObject.Mode()
+ self.state = TransmissionControlObject.State()
+ self.send_queue = collections.deque()
+ self.recv_queue = collections.deque()
+ self.send_ready = threading.Condition(self.lock)
+ self.recv_ready = threading.Condition(self.lock)
+ self.recv_miu = recv_miu
+ self.send_miu = send_miu
+ self.recv_buf = 1
+ self.send_buf = 1
+ self.addr = None
+ self.peer = None
+
+ @property
+ def is_bound(self):
+ return self.addr is not None
+
+ def setsockopt(self, option, value):
+ if option == nfc.llcp.SO_SNDBUF:
+ # with self.lock: self.send_buf = int(value)
+ # adjustable send buffer only with non-blocking socket mode
+ raise NotImplementedError("SO_SNDBUF can not be set")
+ elif option == nfc.llcp.SO_RCVBUF:
+ with self.lock:
+ self.recv_buf = int(value)
+ else:
+ raise ValueError("invalid option value")
+
+ def getsockopt(self, option):
+ if option == nfc.llcp.SO_SNDMIU:
+ return self.send_miu
+ if option == nfc.llcp.SO_RCVMIU:
+ return self.recv_miu
+ if option == nfc.llcp.SO_SNDBUF:
+ return self.send_buf
+ if option == nfc.llcp.SO_RCVBUF:
+ return self.recv_buf
+
+ def bind(self, addr):
+ if self.addr and addr and self.addr != addr:
+ log.warning("socket rebound from {} to {}".format(self.addr, addr))
+ self.addr = addr
+ return self.addr
+
+ def poll(self, event, timeout):
+ if event == "recv":
+ with self.recv_ready:
+ if len(self.recv_queue) == 0:
+ self.recv_ready.wait(timeout)
+ if len(self.recv_queue) > 0:
+ return self.recv_queue[0]
+ return None
+ if event == "send":
+ with self.send_ready:
+ if len(self.send_queue) >= self.send_buf:
+ self.send_ready.wait(timeout)
+ return len(self.send_queue) < self.send_buf
+
+ def send(self, send_pdu, flags):
+ with self.send_ready:
+ self.send_queue.append(send_pdu)
+ if not (flags & nfc.llcp.MSG_DONTWAIT):
+ self.send_ready.wait()
+
+ def recv(self):
+ with self.recv_ready:
+ try:
+ return self.recv_queue.popleft()
+ except IndexError:
+ self.recv_ready.wait()
+ return self.recv_queue.popleft()
+
+ def close(self):
+ with self.lock:
+ self.send_queue.clear()
+ self.recv_queue.clear()
+ self.send_ready.notify_all()
+ self.recv_ready.notify_all()
+ self.state.SHUTDOWN = True
+
+ #
+ # enqueue() and dequeue() are called from llc run thread
+ #
+ def enqueue(self, rcvd_pdu):
+ with self.lock:
+ if len(self.recv_queue) < self.recv_buf:
+ log.debug("enqueue {0}".format(rcvd_pdu))
+ self.recv_queue.append(rcvd_pdu)
+ self.recv_ready.notify()
+ return True
+ else:
+ log.warning("discard {0}".format(rcvd_pdu))
+ return False
+
+ def dequeue(self, miu_size, icv_size, notify=True):
+ # Return the first pending outbound PDU if it's information
+ # field size (total size - header size) does not exceed the
+ # given miu_size value. For UI and I PDUs do also consider the
+ # icv_size value (this is set to non-zero by the packet
+ # collector when aggregating). Re-insert the PDU at the
+ # beginning of the send queue if it exceeds the miu_size.
+ # Skip the length check if miu_size is None.
+ with self.lock:
+ try:
+ send_pdu = self.send_queue.popleft()
+ log.debug("dequeue {0}".format(send_pdu))
+ except IndexError:
+ return None
+
+ if send_pdu.name in ("UI", "I"):
+ pdu_size = len(send_pdu) + icv_size
+ else:
+ pdu_size = len(send_pdu)
+
+ if ((miu_size is not None and
+ pdu_size - send_pdu.header_size > miu_size)):
+ log.debug("requeue {0}".format(send_pdu))
+ self.send_queue.appendleft(send_pdu)
+ return None
+
+ if notify is True:
+ self.send_ready.notify()
+
+ return send_pdu
+
+
+class RawAccessPoint(TransmissionControlObject):
+ """
+ ============= =========== ============
+ State Event Transition
+ ============= =========== ============
+ SHUTDOWN init() ESTABLISHED
+ ESTABLISHED close() SHUTDOWN
+ ============= =========== ============
+ """
+ def __init__(self, recv_miu):
+ super(RawAccessPoint, self).__init__(128, recv_miu)
+ self.state.ESTABLISHED = True
+
+ def __str__(self):
+ return "RAW {:2} -> ?".format(self.addr
+ if self.addr is not None
+ else "None")
+
+ def setsockopt(self, option, value):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ super(RawAccessPoint, self).setsockopt(option, value)
+
+ def getsockopt(self, option):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ return super(RawAccessPoint, self).getsockopt(option)
+
+ def poll(self, event, timeout):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ if event not in ("recv", "send"):
+ raise err.Error(errno.EINVAL)
+ return super(RawAccessPoint, self).poll(event, timeout) is not None
+
+ def send(self, send_pdu, flags):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ log.debug("{0} send {1}".format(str(self), send_pdu))
+ super(RawAccessPoint, self).send(send_pdu, flags)
+ return self.state.ESTABLISHED is True
+
+ def recv(self):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ try:
+ return super(RawAccessPoint, self).recv()
+ except IndexError:
+ raise err.Error(errno.EPIPE)
+
+ def close(self):
+ super(RawAccessPoint, self).close()
+
+ #
+ # enqueue() and dequeue() are called from llc run thread
+ #
+ def enqueue(self, rcvd_pdu):
+ return super(RawAccessPoint, self).enqueue(rcvd_pdu)
+
+ def dequeue(self, miu_size, icv_size):
+ return super(RawAccessPoint, self).dequeue(miu_size=None, icv_size=0)
+
+
+class LogicalDataLink(TransmissionControlObject):
+ """
+ ============= =========== ============
+ State Event Transition
+ ============= =========== ============
+ SHUTDOWN init() ESTABLISHED
+ ESTABLISHED close() SHUTDOWN
+ ============= =========== ============
+ """
+ def __init__(self, recv_miu):
+ super(LogicalDataLink, self).__init__(128, recv_miu)
+ self.state.ESTABLISHED = True
+
+ def __str__(self):
+ return "LDL {addr:2} -> {peer:2}".format(
+ addr=self.addr if self.addr is not None else "None",
+ peer=self.peer if self.peer is not None else "None"
+ )
+
+ def setsockopt(self, option, value):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ super(LogicalDataLink, self).setsockopt(option, value)
+
+ def getsockopt(self, option):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ return super(LogicalDataLink, self).getsockopt(option)
+
+ def connect(self, dest):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ with self.lock:
+ self.peer = dest
+ return self.peer > 0
+
+ def poll(self, event, timeout):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ if event not in ("recv", "send"):
+ raise err.Error(errno.EINVAL)
+ return super(LogicalDataLink, self).poll(event, timeout) is not None
+
+ def sendto(self, message, dest, flags):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ if self.peer and dest != self.peer:
+ raise err.Error(errno.EDESTADDRREQ)
+ if len(message) > self.send_miu:
+ raise err.Error(errno.EMSGSIZE)
+ send_pdu = pdu.UnnumberedInformation(dest, self.addr, data=message)
+ super(LogicalDataLink, self).send(send_pdu, flags)
+ return self.state.ESTABLISHED is True
+
+ def recvfrom(self):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ try:
+ rcvd_pdu = super(LogicalDataLink, self).recv()
+ except IndexError:
+ raise err.Error(errno.EPIPE)
+ return (rcvd_pdu.data, rcvd_pdu.ssap) if rcvd_pdu else (None, None)
+
+ def close(self):
+ super(LogicalDataLink, self).close()
+
+ #
+ # enqueue() and dequeue() are called from llc run thread
+ #
+ def enqueue(self, rcvd_pdu):
+ if not rcvd_pdu.name == "UI":
+ log.warning("ignore %s PDU on logical data link", rcvd_pdu.name)
+ return False
+ if len(rcvd_pdu.data) > self.recv_miu:
+ log.warning("received UI PDU exceeds local link MIU")
+ return False
+ return super(LogicalDataLink, self).enqueue(rcvd_pdu)
+
+ def dequeue(self, miu_size, icv_size):
+ return super(LogicalDataLink, self).dequeue(miu_size, icv_size)
+
+
+class DataLinkConnection(TransmissionControlObject):
+ """
+ ============= =========== ============
+ State Event Transition
+ ============= =========== ============
+ SHUTDOWN init() ESTABLISHED
+ CLOSED listen() LISTEN
+ CLOSED connect() CONNECT
+ CONNECT CC-PDU ESTABLISHED
+ CONNECT DM-PDU CLOSED
+ ESTABLISHED I-PDU ESTABLISHED
+ ESTABLISHED RR-PDU ESTABLISHED
+ ESTABLISHED RNR-PDU ESTABLISHED
+ ESTABLISHED FRMR-PDU SHUTDOWN
+ ESTABLISHED DISC-PDU CLOSE_WAIT
+ ESTABLISHED close() SHUTDOWN
+ CLOSE_WAIT close() SHUTDOWN
+ ============= =========== ============
+ """
+
+ DLC_PDU_NAMES = ("CONNECT", "DISC", "CC", "DM", "FRMR", "I", "RR", "RNR")
+
+ def __init__(self, recv_miu, recv_win):
+ super(DataLinkConnection, self).__init__(128, recv_miu)
+ self.state.CLOSED = True
+ self.acks_ready = threading.Condition(self.lock)
+ self.acks_recvd = 0 # received acknowledgements
+ self.recv_confs = 0 # outstanding receive confirmations
+ self.send_token = threading.Condition(self.lock)
+ self.recv_buf = recv_win
+ self.recv_win = recv_win # RW(Local)
+ self.recv_cnt = 0 # V(R)
+ self.recv_ack = 0 # V(RA)
+ self.send_win = None # RW(Remote)
+ self.send_cnt = 0 # V(S)
+ self.send_ack = 0 # V(SA)
+
+ def __str__(self):
+ s = "DLC {addr:2} <-> {peer:2} {dlc.state} "
+ s += "RW(R)={dlc.send_win} V(S)={dlc.send_cnt} V(SA)={dlc.send_ack} "
+ s += "RW(L)={dlc.recv_win} V(R)={dlc.recv_cnt} V(RA)={dlc.recv_ack}"
+ return s.format(
+ dlc=self,
+ addr=self.addr if self.addr is not None else "None",
+ peer=self.peer if self.peer is not None else "None"
+ )
+
+ def log(self, string):
+ log.debug("DLC ({dlc.addr},{dlc.peer}) {dlc.state} {s}"
+ .format(dlc=self, s=string))
+
+ def err(self, string):
+ log.error("DLC ({dlc.addr},{dlc.peer}) {s}".format(dlc=self, s=string))
+
+ def setsockopt(self, option, value):
+ with self.lock:
+ if option == nfc.llcp.SO_RCVMIU and self.state.CLOSED:
+ self.recv_miu = min(value, 2175)
+ return
+ if option == nfc.llcp.SO_RCVBUF and self.state.CLOSED:
+ self.recv_win = min(value, 15)
+ self.recv_buf = self.recv_win
+ return
+ if option == nfc.llcp.SO_RCVBSY:
+ self.mode.RECV_BUSY = bool(value)
+ return
+ super(DataLinkConnection, self).setsockopt(option, value)
+
+ def getsockopt(self, option):
+ if option == nfc.llcp.SO_RCVBUF:
+ return self.recv_win
+ if option == nfc.llcp.SO_SNDBSY:
+ return self.mode.SEND_BUSY
+ if option == nfc.llcp.SO_RCVBSY:
+ return self.mode.RECV_BUSY
+ return super(DataLinkConnection, self).getsockopt(option)
+
+ def listen(self, backlog):
+ with self.lock:
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ if not self.state.CLOSED:
+ self.err("listen() but socket state is {0}".format(self.state))
+ raise err.Error(errno.ENOTSUP)
+ self.state.LISTEN = True
+ self.recv_buf = backlog
+
+ def accept(self):
+ with self.lock:
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+ if not self.state.LISTEN:
+ self.err("accept() but socket state is {0}".format(self.state))
+ raise err.Error(errno.EINVAL)
+ self.recv_buf += 1
+ try:
+ rcvd_pdu = super(DataLinkConnection, self).recv()
+ except IndexError:
+ raise err.Error(errno.EPIPE)
+ self.recv_buf -= 1
+ if rcvd_pdu.name == "CONNECT":
+ dlc = DataLinkConnection(self.recv_miu, self.recv_win)
+ dlc.addr = self.addr
+ dlc.peer = rcvd_pdu.ssap
+ dlc.send_miu = rcvd_pdu.miu
+ dlc.send_win = rcvd_pdu.rw
+ send_pdu = pdu.ConnectionComplete(dlc.peer, dlc.addr)
+ send_pdu.miu, send_pdu.rw = dlc.recv_miu, dlc.recv_win
+ log.debug("accepting CONNECT from SAP %d" % dlc.peer)
+ dlc.state.ESTABLISHED = True
+ self.send_queue.append(send_pdu)
+ return dlc
+ else: # pragma: no cover
+ raise RuntimeError("CONNECT expected, not " + rcvd_pdu.name)
+
+ def connect(self, dest):
+ with self.lock:
+ if not self.state.CLOSED:
+ self.err("connect() in socket state {0}".format(self.state))
+ if self.state.ESTABLISHED:
+ raise err.Error(errno.EISCONN)
+ if self.state.CONNECT:
+ raise err.Error(errno.EALREADY)
+ raise err.Error(errno.EPIPE)
+ if isinstance(dest, (bytes, bytearray)):
+ send_pdu = pdu.Connect(1, self.addr, self.recv_miu,
+ self.recv_win, bytes(dest))
+ elif isinstance(dest, str):
+ send_pdu = pdu.Connect(1, self.addr, self.recv_miu,
+ self.recv_win, dest.encode('latin'))
+ elif isinstance(dest, int):
+ send_pdu = pdu.Connect(dest, self.addr, self.recv_miu,
+ self.recv_win)
+ else:
+ raise TypeError("connect destination must be int or bytes")
+
+ self.state.CONNECT = True
+ self.send_queue.append(send_pdu)
+
+ try:
+ rcvd_pdu = super(DataLinkConnection, self).recv()
+ except IndexError:
+ raise err.Error(errno.EPIPE)
+
+ if rcvd_pdu.name == "DM":
+ logstr = "connect rejected with reason {}"
+ self.log(logstr.format(rcvd_pdu.reason))
+ self.state.CLOSED = True
+ raise err.ConnectRefused(rcvd_pdu.reason)
+ elif rcvd_pdu.name == "CC":
+ self.peer = rcvd_pdu.ssap
+ self.recv_buf = self.recv_win
+ self.send_miu = rcvd_pdu.miu
+ self.send_win = rcvd_pdu.rw
+ self.state.ESTABLISHED = True
+ return
+ else: # pragma: no cover
+ raise RuntimeError("CC or DM expected, not " + rcvd_pdu.name)
+
+ @property
+ def send_window_slots(self):
+ # RW(R) - V(S) + V(SA) mod 16
+ return (self.send_win - self.send_cnt + self.send_ack) % 16
+
+ @property
+ def recv_window_slots(self):
+ # RW(L) - V(R) + V(RA) mod 16
+ return (self.recv_win - self.recv_cnt + self.recv_ack) % 16
+
+ def send(self, message, flags):
+ with self.send_token:
+ if not self.state.ESTABLISHED:
+ self.err("send() in socket state {0}".format(self.state))
+ if self.state.CLOSE_WAIT:
+ raise err.Error(errno.EPIPE)
+ raise err.Error(errno.ENOTCONN)
+ if len(message) > self.send_miu:
+ raise err.Error(errno.EMSGSIZE)
+ while self.send_window_slots == 0 and self.state.ESTABLISHED:
+ if flags & nfc.llcp.MSG_DONTWAIT:
+ raise err.Error(errno.EWOULDBLOCK)
+ self.log("waiting on busy send window")
+ self.send_token.wait()
+ self.log("send {0} byte on {1}".format(len(message), str(self)))
+ if self.state.ESTABLISHED:
+ send_pdu = pdu.Information(self.peer, self.addr, data=message)
+ send_pdu.ns = self.send_cnt
+ self.send_cnt = (self.send_cnt + 1) % 16
+ super(DataLinkConnection, self).send(send_pdu, flags)
+ return self.state.ESTABLISHED is True
+
+ def recv(self):
+ with self.lock:
+ if not (self.state.ESTABLISHED or self.state.CLOSE_WAIT):
+ self.err("recv() in socket state {0}".format(self.state))
+ raise err.Error(errno.ENOTCONN)
+
+ try:
+ rcvd_pdu = super(DataLinkConnection, self).recv()
+ except IndexError:
+ return None
+
+ if rcvd_pdu.name == "I":
+ self.recv_confs += 1
+ if self.recv_confs > self.recv_win:
+ self.err("recv_confs({0}) > recv_win({1})"
+ .format(self.recv_confs, self.recv_win))
+ raise RuntimeError("recv_confs > recv_win")
+ return rcvd_pdu.data
+
+ if rcvd_pdu.name == "DISC":
+ self.close()
+ return None
+
+ raise RuntimeError("only I or DISC expected, not " + rcvd_pdu.name)
+
+ def poll(self, event, timeout):
+ if self.state.SHUTDOWN:
+ raise err.Error(errno.ESHUTDOWN)
+
+ if event == "recv":
+ if self.state.ESTABLISHED or self.state.CLOSE_WAIT:
+ rcvd_pdu = super(DataLinkConnection, self).poll(event, timeout)
+ if self.state.ESTABLISHED or self.state.CLOSE_WAIT:
+ return isinstance(rcvd_pdu, pdu.Information)
+ elif event == "send":
+ if self.state.ESTABLISHED:
+ if super(DataLinkConnection, self).poll(event, timeout):
+ return self.state.ESTABLISHED
+ return False
+ elif event == "acks":
+ with self.acks_ready:
+ if not self.acks_recvd > 0:
+ self.acks_ready.wait(timeout)
+ if self.acks_recvd > 0:
+ self.acks_recvd = self.acks_recvd - 1
+ return True
+ return False
+ else:
+ raise err.Error(errno.EINVAL)
+
+ def close(self):
+ with self.lock:
+ self.log("close()")
+ if self.state.ESTABLISHED and self.is_bound:
+ self.state.DISCONNECT = True
+ self.send_token.notify_all()
+ self.acks_ready.notify_all()
+ send_pdu = pdu.Disconnect(self.peer, self.addr)
+ self.send_queue.append(send_pdu)
+ try:
+ super(DataLinkConnection, self).recv()
+ except IndexError:
+ pass
+ super(DataLinkConnection, self).close()
+ self.acks_ready.notify_all()
+ self.send_token.notify_all()
+
+ #
+ # enqueue() and dequeue() are called from llc thread context
+ #
+ def enqueue(self, rcvd_pdu):
+ self.log("enqueue {pdu.name} PDU".format(pdu=rcvd_pdu))
+
+ if rcvd_pdu.name not in self.DLC_PDU_NAMES:
+ self.err("non connection mode pdu on data link connection")
+ send_pdu = pdu.FrameReject.from_pdu(rcvd_pdu, flags="W", dlc=self)
+ self.close()
+ self.send_queue.append(send_pdu)
+ return
+
+ if self.state.CLOSED:
+ self.send_queue.append(pdu.DisconnectedMode(
+ rcvd_pdu.ssap, rcvd_pdu.dsap, reason=1))
+
+ elif self.state.LISTEN and rcvd_pdu.name == "CONNECT":
+ if super(DataLinkConnection, self).enqueue(rcvd_pdu) is False:
+ log.warning("full backlog on listening socket")
+ self.send_queue.append(pdu.DisconnectedMode(
+ rcvd_pdu.ssap, rcvd_pdu.dsap, reason=0x20))
+
+ elif self.state.CONNECT and rcvd_pdu.name in ("CC", "DM"):
+ with self.lock:
+ self.recv_queue.append(rcvd_pdu)
+ self.recv_ready.notify()
+
+ elif self.state.DISCONNECT and rcvd_pdu.name == "DM":
+ with self.lock:
+ self.recv_queue.append(rcvd_pdu)
+ self.recv_ready.notify()
+
+ elif self.state.ESTABLISHED:
+ return self._enqueue_state_established(rcvd_pdu)
+
+ def _enqueue_state_established(self, rcvd_pdu):
+ if rcvd_pdu.name == "I":
+ frmr = None
+ if len(rcvd_pdu.data) > self.recv_miu:
+ frmr = pdu.FrameReject.from_pdu(rcvd_pdu, flags="I", dlc=self)
+ elif rcvd_pdu.ns != self.recv_cnt:
+ frmr = pdu.FrameReject.from_pdu(rcvd_pdu, flags="S", dlc=self)
+ if frmr:
+ self.log("reject " + str(self))
+ self.send_queue.clear()
+ self.send_queue.append(frmr)
+ log.debug("enqueued frame reject pdu")
+ return
+
+ if rcvd_pdu.name == "FRMR":
+ with self.lock:
+ self.state.SHUTDOWN = True
+ self.close()
+ return
+
+ if rcvd_pdu.name == "DISC":
+ with self.lock:
+ self.state.CLOSE_WAIT = True
+ self.send_queue.clear()
+ self.send_queue.append(pdu.DisconnectedMode(
+ self.peer, self.addr, reason=0))
+ return
+
+ if rcvd_pdu.name in ("I", "RR", "RNR"):
+ with self.lock:
+ # acks = N(R) - V(SA) mod 16
+ acks = (rcvd_pdu.nr - self.send_ack) % 16
+ if acks:
+ self.acks_recvd += acks
+ self.acks_ready.notify_all()
+ self.send_token.notify()
+ self.send_ack = rcvd_pdu.nr # V(SA) := N(R)
+ if rcvd_pdu.name == "RNR":
+ self.mode.SEND_BUSY = True
+ if rcvd_pdu.name == "RR":
+ self.mode.SEND_BUSY = False
+
+ if rcvd_pdu.name == "I":
+ with self.lock:
+ # V(R) := V(R) + 1 mod 16
+ self.recv_cnt = (self.recv_cnt + 1) % 16
+ super(DataLinkConnection, self).enqueue(rcvd_pdu)
+
+ def dequeue(self, miu_size, icv_size):
+ with self.lock:
+ if self.state.ESTABLISHED:
+ if self.mode.RECV_BUSY_SENT != self.mode.RECV_BUSY:
+ self.mode.RECV_BUSY_SENT = self.mode.RECV_BUSY
+ ACK = RNR_PDU if self.mode.RECV_BUSY else RR_PDU
+ return ACK(self.peer, self.addr, self.recv_ack)
+
+ send_pdu = super(DataLinkConnection, self).dequeue(
+ miu_size, icv_size, notify=False)
+
+ if send_pdu:
+ self.log("dequeue {0} PDU".format(send_pdu.name))
+
+ if send_pdu.name == "FRMR":
+ self.state.SHUTDOWN = True
+ self.close()
+
+ if send_pdu.name == "I" and self.state.ESTABLISHED:
+ if self.recv_confs and self.recv_cnt != self.recv_ack:
+ self.log("piggyback ack " + str(self))
+ self.recv_ack = (self.recv_ack + self.recv_confs) % 16
+ self.recv_confs = 0
+ send_pdu.nr = self.recv_ack
+ self.send_ready.notify()
+
+ if send_pdu.name == "DM" and self.state.CLOSE_WAIT:
+ self.recv_queue.append(pdu.Disconnect(
+ dsap=self.peer, ssap=self.addr))
+ self.recv_ready.notify()
+ self.send_token.notify_all()
+
+ else:
+ if ((self.state.ESTABLISHED and self.recv_confs
+ and self.recv_window_slots == 0)):
+ # must send acknowledgement to keep going
+ self.log("necessary ack " + str(self))
+ self.recv_ack = (self.recv_ack + self.recv_confs) % 16
+ self.recv_confs = 0
+ ACK = RNR_PDU if self.mode.RECV_BUSY else RR_PDU
+ return ACK(self.peer, self.addr, self.recv_ack)
+
+ return send_pdu
+
+ def sendack(self):
+ if self.state.ESTABLISHED:
+ with self.lock:
+ if self.recv_confs and self.recv_cnt != self.recv_ack:
+ self.log("voluntary ack " + str(self))
+ self.recv_ack = (self.recv_ack + self.recv_confs) % 16
+ self.recv_confs = 0
+ ACK = RNR_PDU if self.mode.RECV_BUSY else RR_PDU
+ return ACK(self.peer, self.addr, self.recv_ack)
+
+
+RR_PDU, RNR_PDU = pdu.ReceiveReady, pdu.ReceiveNotReady
diff --git a/src/lib/nfc/snep/__init__.py b/src/lib/nfc/snep/__init__.py
new file mode 100644
index 0000000..9e2145c
--- /dev/null
+++ b/src/lib/nfc/snep/__init__.py
@@ -0,0 +1,36 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+"""
+The nfc.snep module implements the NFC Forum Simple NDEF Exchange
+Protocol (SNEP) specification and provides a server and client class
+for applications to easily send or receive SNEP messages.
+"""
+from src.lib.nfc.snep.server import SnepServer # noqa: F401
+from src.lib.nfc.snep.client import SnepClient # noqa: F401
+from src.lib.nfc.snep.client import SnepError # noqa: F401
+
+Success = 0x81
+NotFound = 0xC0
+ExcessData = 0xC1
+BadRequest = 0xC2
+NotImplemented = 0xE0
+UnsupportedVersion = 0xE1
diff --git a/src/lib/nfc/snep/client.py b/src/lib/nfc/snep/client.py
new file mode 100644
index 0000000..73a2ade
--- /dev/null
+++ b/src/lib/nfc/snep/client.py
@@ -0,0 +1,247 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+#
+# Simple NDEF Exchange Protocol (SNEP) - Client Base Class
+#
+import ndef
+import struct
+import src.lib.nfc.llcp
+
+import logging
+log = logging.getLogger(__name__)
+
+
+def send_request(socket, snep_request, send_miu):
+ if len(snep_request) <= send_miu:
+ return socket.send(snep_request)
+
+ if not socket.send(snep_request[0:send_miu]):
+ return False
+
+ if socket.recv() != b"\x10\x80\x00\x00\x00\x00":
+ return False
+
+ for offset in range(send_miu, len(snep_request), send_miu):
+ fragment = snep_request[offset:offset+send_miu]
+ if not socket.send(fragment):
+ return False
+
+ return True
+
+
+def recv_response(socket, acceptable_length, timeout):
+ if socket.poll("recv", timeout):
+ snep_response = socket.recv()
+
+ if len(snep_response) < 6:
+ log.debug("snep response initial fragment too short")
+ return None
+
+ version, status, length = struct.unpack(">BBL", snep_response[:6])
+
+ if length > acceptable_length:
+ log.debug("snep response exceeds acceptable length")
+ return None
+
+ if len(snep_response) - 6 < length:
+ # request remaining fragments
+ socket.send(b"\x10\x00\x00\x00\x00\x00")
+ while len(snep_response) - 6 < length:
+ if socket.poll("recv", timeout):
+ snep_response += socket.recv()
+ else:
+ return None
+
+ return bytearray(snep_response)
+
+
+class SnepClient(object):
+ """ Simple NDEF exchange protocol - client implementation
+ """
+ def __init__(self, llc, max_ndef_msg_recv_size=1024):
+ self.acceptable_length = max_ndef_msg_recv_size
+ self.socket = None
+ self.llc = llc
+
+ def connect(self, service_name):
+ """Connect to a SNEP server. This needs only be called to
+ connect to a server other than the Default SNEP Server at
+ `urn:nfc:sn:snep` or if the client wants to send multiple
+ requests with a single connection.
+ """
+ self.close()
+ self.socket = nfc.llcp.Socket(self.llc, nfc.llcp.DATA_LINK_CONNECTION)
+ self.socket.connect(service_name)
+ self.send_miu = self.socket.getsockopt(nfc.llcp.SO_SNDMIU)
+
+ def close(self):
+ """Close the data link connection with the SNEP server.
+ """
+ if self.socket:
+ self.socket.close()
+ self.socket = None
+
+ def get_records(self, records=None, timeout=1.0):
+ """Get NDEF message records from a SNEP Server.
+
+ .. versionadded:: 0.13
+
+ The :class:`ndef.Record` list given by *records* is encoded as
+ the request message octets input to :meth:`get_octets`. The
+ return value is an :class:`ndef.Record` list decoded from the
+ response message octets returned by :meth:`get_octets`. Same
+ as::
+
+ import ndef
+ send_octets = ndef.message_encoder(records)
+ rcvd_octets = snep_client.get_octets(send_octets, timeout)
+ records = list(ndef.message_decoder(rcvd_octets))
+
+ """
+ octets = b''.join(ndef.message_encoder(records)) if records else None
+ octets = self.get_octets(octets, timeout)
+ if octets and len(octets) >= 3:
+ return list(ndef.message_decoder(octets))
+
+ def get_octets(self, octets=None, timeout=1.0):
+ """Get NDEF message octets from a SNEP Server.
+
+ .. versionadded:: 0.13
+
+ If the client has not yet a data link connection with a SNEP
+ Server, it temporarily connects to the default SNEP Server,
+ sends the message octets, disconnects after the server
+ response, and returns the received message octets.
+
+ """
+ if octets is None:
+ # Send NDEF Message with one empty Record.
+ octets = b'\xd0\x00\x00'
+
+ if not self.socket:
+ try:
+ self.connect('urn:nfc:sn:snep')
+ except nfc.llcp.ConnectRefused:
+ return None
+ else:
+ self.release_connection = True
+ else:
+ self.release_connection = False
+
+ try:
+ request = struct.pack('>BBLL', 0x10, 0x01, 4 + len(octets),
+ self.acceptable_length) + octets
+
+ if not send_request(self.socket, request, self.send_miu):
+ return None
+
+ response = recv_response(
+ self.socket, self.acceptable_length, timeout)
+
+ if response is not None:
+ if response[1] != 0x81:
+ raise SnepError(response[1])
+
+ return response[6:]
+
+ finally:
+ if self.release_connection:
+ self.close()
+
+ def put_records(self, records, timeout=1.0):
+ """Send NDEF message records to a SNEP Server.
+
+ .. versionadded:: 0.13
+
+ The :class:`ndef.Record` list given by *records* is encoded
+ and then send via :meth:`put_octets`. Same as::
+
+ import ndef
+ octets = ndef.message_encoder(records)
+ snep_client.put_octets(octets, timeout)
+
+ """
+ octets = b''.join(ndef.message_encoder(records))
+ return self.put_octets(octets, timeout)
+
+ def put_octets(self, octets, timeout=1.0):
+ """Send NDEF message octets to a SNEP Server.
+
+ .. versionadded:: 0.13
+
+ If the client has not yet a data link connection with a SNEP
+ Server, it temporarily connects to the default SNEP Server,
+ sends the message octets and disconnects after the server
+ response.
+
+ """
+ if not self.socket:
+ try:
+ self.connect('urn:nfc:sn:snep')
+ except nfc.llcp.ConnectRefused:
+ return False
+ else:
+ self.release_connection = True
+ else:
+ self.release_connection = False
+
+ try:
+ request = struct.pack('>BBL', 0x10, 0x02, len(octets)) + octets
+ if not send_request(self.socket, request, self.send_miu):
+ return False
+
+ response = recv_response(self.socket, 0, timeout)
+ if response is not None:
+ if response[1] != 0x81:
+ raise SnepError(response[1])
+
+ return True
+
+ finally:
+ if self.release_connection:
+ self.close()
+
+ def __enter__(self):
+ self.connect()
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.close()
+
+
+class SnepError(Exception):
+ strerr = {0xC0: "resource not found",
+ 0xC1: "resource exceeds data size limit",
+ 0xC2: "malformed request not understood",
+ 0xE0: "unsupported functionality requested",
+ 0xE1: "unsupported protocol version"}
+
+ def __init__(self, err):
+ self.args = (err, SnepError.strerr.get(err, ""))
+
+ def __str__(self):
+ return "nfc.snep.SnepError: [{errno}] {info}".format(
+ errno=self.args[0], info=self.args[1])
+
+ @property
+ def errno(self):
+ return self.args[0]
diff --git a/src/lib/nfc/snep/server.py b/src/lib/nfc/snep/server.py
new file mode 100644
index 0000000..496e437
--- /dev/null
+++ b/src/lib/nfc/snep/server.py
@@ -0,0 +1,175 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+#
+# Simple NDEF Exchange Protocol (SNEP) - Server Base Class
+#
+import threading
+import binascii
+import logging
+import struct
+import errno
+import ndef
+import src.lib.nfc
+
+
+log = logging.getLogger(__name__)
+
+
+class SnepServer(threading.Thread):
+ """ NFC Forum Simple NDEF Exchange Protocol server
+ """
+ def __init__(self, llc, service_name="urn:nfc:sn:snep",
+ max_acceptable_length=0x100000,
+ recv_miu=1984, recv_buf=15):
+
+ self.max_acceptable_length = min(max_acceptable_length, 0xFFFFFFFF)
+ socket = nfc.llcp.Socket(llc, nfc.llcp.DATA_LINK_CONNECTION)
+ recv_miu = socket.setsockopt(nfc.llcp.SO_RCVMIU, recv_miu)
+ recv_buf = socket.setsockopt(nfc.llcp.SO_RCVBUF, recv_buf)
+ socket.bind(service_name)
+ log.info("snep server bound to port {0} (MIU={1}, RW={2}), "
+ "will accept up to {3} byte NDEF messages"
+ .format(socket.getsockname(), recv_miu, recv_buf,
+ self.max_acceptable_length))
+ socket.listen(backlog=2)
+ threading.Thread.__init__(self, name=service_name,
+ target=self._listen, args=(socket,))
+
+ def _listen(self, listen_socket):
+ try:
+ while True:
+ client_socket = listen_socket.accept()
+ client_thread = threading.Thread(target=self._serve,
+ args=(client_socket,))
+ client_thread.start()
+ except nfc.llcp.Error as error:
+ (log.debug if error.errno == errno.EPIPE else log.error)(error)
+ finally:
+ listen_socket.close()
+
+ def _serve(self, client_socket):
+ peer_sap = client_socket.getpeername()
+ log.info("serving snep client on remote sap {0}".format(peer_sap))
+ send_miu = client_socket.getsockopt(nfc.llcp.SO_SNDMIU)
+ try:
+ while client_socket.poll('recv'):
+ data = bytearray(client_socket.recv())
+ if not data:
+ break # connection closed
+
+ if len(data) < 6:
+ log.debug("snep msg initial fragment too short")
+ break # bail out, this is a bad client
+
+ version, length = struct.unpack_from(">BxL", data)
+
+ if (version >> 4) > 1:
+ log.debug("unsupported version {0}".format(version >> 4))
+ client_socket.send(b"\x10\xE1\x00\x00\x00\x00")
+ continue
+
+ if length > self.max_acceptable_length:
+ log.debug("snep msg exceeds max acceptable length")
+ client_socket.send(b"\x10\xFF\x00\x00\x00\x00")
+ continue
+
+ if len(data) - 6 < length:
+ # request remaining fragments
+ client_socket.send(b"\x10\x80\x00\x00\x00\x00")
+ while len(data) - 6 < length:
+ try:
+ data += client_socket.recv()
+ except TypeError:
+ break # connection closed
+
+ # message complete, now handle the request
+ data = self.process_snep_request(data)
+
+ # send the snep response, fragment if needed
+ if len(data) <= send_miu:
+ client_socket.send(data)
+ else:
+ client_socket.send(data[0:send_miu])
+ if client_socket.recv() == b"\x10\x00\x00\x00\x00\x00":
+ parts = range(send_miu, len(data), send_miu)
+ for offset in parts:
+ client_socket.send(data[offset:offset + send_miu])
+
+ except nfc.llcp.Error as e:
+ (log.debug if e.errno == nfc.llcp.errno.EPIPE else log.error)(e)
+ finally:
+ client_socket.close()
+
+ def process_snep_request(self, request_data):
+ assert isinstance(request_data, bytearray)
+ log.debug("<<< %s", binascii.hexlify(request_data).decode())
+ try:
+ if request_data[1] == 1 and len(request_data) >= 10:
+ acceptable_length = struct.unpack(">L", request_data[6:10])[0]
+ octets = request_data[10:]
+ records = list(ndef.message_decoder(octets, known_types={}))
+ response = self.process_get_request(records)
+ if isinstance(response, int):
+ response_code = response
+ response_data = b''
+ else:
+ response_code = 0x81 # nfc.snep.Success
+ response_data = b''.join(ndef.message_encoder(response))
+ if len(response_data) > acceptable_length:
+ response_code = 0xC1 # nfc.snep.ExcessData
+ response_data = b''
+ elif request_data[1] == 2:
+ octets = request_data[6:]
+ records = list(ndef.message_decoder(octets, known_types={}))
+ response_code = self.process_put_request(records)
+ response_data = b''
+ else:
+ log.debug("bad request (0x{:02x})".format(request_data[1]))
+ response_code = 0xC2 # nfc.snep.BadRequest
+ response_data = b''
+ except ndef.DecodeError as error:
+ log.error(repr(error))
+ response_code = 0xC2 # nfc.snep.BadRequest
+ response_data = b''
+ except ndef.EncodeError as error:
+ log.error(repr(error))
+ response_code = 0xC0 # nfc.snep.NotFound
+ response_data = b''
+
+ header = struct.pack(">BBL", 0x10, response_code, len(response_data))
+ response_data = header + response_data
+ log.debug(">>> %s", binascii.hexlify(response_data).decode())
+ return response_data
+
+ def process_get_request(self, ndef_message):
+ """Handle Get requests. This method should be overwritten by a
+ subclass of SnepServer to customize it's behavior. The default
+ implementation simply returns nfc.snep.NotImplemented.
+ """
+ return 0xE0 # NotImplemented
+
+ def process_put_request(self, ndef_message):
+ """Process a SNEP Put request. This method should be overwritten by a
+ subclass of SnepServer to customize it's behavior. The default
+ implementation simply returns nfc.snep.Success.
+ """
+ return 0x81 # nfc.snep.Success
diff --git a/src/lib/nfc/tag/__init__.py b/src/lib/nfc/tag/__init__.py
new file mode 100644
index 0000000..9229653
--- /dev/null
+++ b/src/lib/nfc/tag/__init__.py
@@ -0,0 +1,480 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2013, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import logging
+from binascii import hexlify
+from ndef import message_decoder, message_encoder
+
+
+logging.captureWarnings(True)
+log = logging.getLogger(__name__)
+
+
+class Tag(object):
+ """The base class for all NFC Tags/Cards. The methods and attributes
+ defined here are commonly available but some may, depending on the
+ tag product, also return a :const:`None` value is support is not
+ available.
+
+ Direct subclasses are the NFC Forum tag types:
+ :class:`~nfc.tag.tt1.Type1Tag`, :class:`~nfc.tag.tt2.Type2Tag`,
+ :class:`~nfc.tag.tt3.Type3Tag`, :class:`~nfc.tag.tt4.Type4Tag`.
+ Some of them are further specialized in vendor/product specific
+ classes.
+
+ """
+ class NDEF(object):
+ """The NDEF object type that may be read from :attr:`Tag.ndef`.
+
+ This class presents the NDEF management information and the
+ actual NDEF message by a couple of attributes. It is normally
+ accessed from a :class:`Tag` instance (further named *tag*)
+ through the :attr:`Tag.ndef` attribute for reading or writing
+ NDEF records. ::
+
+ if tag.ndef is not None:
+ for record in tag.ndef.records:
+ print(record)
+ if tag.ndef.is_writeable:
+ from ndef import TextRecord
+ tag.ndef.records = [TextRecord("Hello World")]
+
+ """
+ def __init__(self, tag):
+ self._tag = tag
+ self._data = None
+ self._capacity = 0
+ self._readable = False
+ self._writeable = False
+
+ def _read_ndef_data(self):
+ msg = "_read_ndef_data is not implemented for this tag type"
+ raise NotImplementedError(msg)
+
+ def _write_ndef_data(self, data):
+ msg = "_write_ndef_data is not implemented for this tag type"
+ raise NotImplementedError(msg)
+
+ @property
+ def tag(self):
+ """A readonly reference to the underlying tag object."""
+ return self._tag
+
+ @property
+ def length(self):
+ """Length of the current NDEF message in bytes."""
+ return len(self._data) if self._data else 0
+
+ @property
+ def capacity(self):
+ """Maximum number of bytes for an NDEF message."""
+ return self._capacity
+
+ @property
+ def is_readable(self):
+ """:const:`True` if the NDEF data are is readable."""
+ return self._readable
+
+ @property
+ def is_writeable(self):
+ """:const:`True` if the NDEF data area is writeable."""
+ return self._writeable
+
+ @property
+ def has_changed(self):
+ """The boolean attribute :attr:`has_changed` allows to determine
+ whether the NDEF message on the tag is different from the
+ message that was read or written at an earlier time in the
+ session. This may for example be the case if the tag is
+ build to dynamically present different content depending
+ on some state.
+
+ Note that reading this attribute involves a complete
+ update of the :class:`Tag.NDEF` instance and it is
+ possible that :attr:`Tag.ndef` is :const:`None` after the
+ update (e.g. tag gone during read or a dynamic tag that
+ failed). A robust implementation should always verify the
+ value of the :attr:`Tag.ndef` attribute. ::
+
+ if tag.ndef.has_changed and tag.ndef is not None:
+ for record in tag.ndef.records:
+ print(record)
+
+ The :attr:`has_changed` attribute can also be used to
+ verify that NDEF records written to the tag are identical
+ to the NDEF records stored on the tag. ::
+
+ from ndef import TextRecord
+ tag.ndef.records = [TextRecord("Hello World")]
+ if tag.ndef.has_changed:
+ print("the tag data differs from what was written")
+
+ """
+ ndef_data = self._read_ndef_data()
+ different = self._data != ndef_data
+ if ndef_data is None:
+ self._tag._ndef = None
+ self._data = ndef_data
+ return different
+
+ @property
+ def records(self):
+ """Read or write a list of NDEF Records.
+
+ .. versionadded:: 0.12
+
+ This attribute is a convinience wrapper for decoding and
+ encoding of the NDEF message data :attr:`octets`. It uses
+ the `ndeflib `_ module to
+ return the list of :class:`ndef.Record` instances decoded
+ from the NDEF message data or set the message data from a
+ list of records. ::
+
+ from ndef import TextRecord
+ if tag.ndef is not None:
+ for record in tag.ndef.records:
+ print(record)
+ try:
+ tag.ndef.records = [TextRecord('Hello World')]
+ except nfc.tag.TagCommandError as err:
+ print("NDEF write failed: " + str(err))
+
+ Decoding is performed with a relaxed error handling
+ strategy that ignores minor errors in the NDEF data. The
+ `ndeflib `_ does also
+ support 'strict' and 'ignore' error handling which may be
+ used like so::
+
+ from ndef import message_decoder, message_encoder
+ records = message_decoder(tag.ndef.octets, errors='strict')
+ tag.ndef.octets = b''.join(message_encoder(records))
+
+ """
+ return list(message_decoder(self.octets, errors='relax'))
+
+ @records.setter
+ def records(self, value):
+ self.octets = b''.join(message_encoder(value))
+
+ @property
+ def octets(self):
+ """Read or write NDEF message data octets.
+
+ .. versionadded:: 0.12
+
+ The *octets* attribute returns the NDEF message data
+ octets as bytes. A bytes or bytearray sequence assigned to
+ *octets* is immediately written to the NDEF message data
+ area, unless the Tag memory is write protected or to
+ small. ::
+
+ if tag.ndef is not None:
+ print(hexlify(tag.ndef.octets).decode())
+
+ """
+ return bytes(self._data)
+
+ @octets.setter
+ def octets(self, data):
+ if not self._writeable:
+ raise AttributeError("tag ndef area is not writeable")
+ data = bytearray(data)
+ if len(data) > self.capacity:
+ raise ValueError("data length exceeds tag capacity")
+ self._write_ndef_data(data)
+ self._data = data
+
+ def __init__(self, clf, target):
+ self._clf, self._target = (clf, target)
+ self._ndef = None
+ self._authenticated = False
+
+ def __str__(self):
+ """x.__str__() <==> str(x)"""
+ try:
+ s = self.type + ' ' + repr(self._product)
+ except AttributeError:
+ s = self.type
+ return "{} ID={}".format(s, hexlify(self.identifier).decode().upper())
+
+ @property
+ def clf(self):
+ return self._clf
+
+ @property
+ def target(self):
+ return self._target
+
+ @property
+ def type(self):
+ return self.TYPE
+
+ @property
+ def product(self):
+ return self._product if hasattr(self, "_product") else self.type
+
+ @property
+ def identifier(self):
+ """The unique tag identifier."""
+ return bytes(self._nfcid)
+
+ @property
+ def ndef(self):
+ """An :class:`NDEF` object if found, otherwise :const:`None`."""
+ if self._ndef is None:
+ ndef = self.NDEF(self)
+ if ndef.has_changed:
+ self._ndef = ndef
+ return self._ndef
+
+ @property
+ def is_present(self):
+ """True if the tag is within communication range."""
+ return self._is_present()
+
+ @property
+ def is_authenticated(self):
+ """True if the tag was successfully authenticated."""
+ return bool(self._authenticated)
+
+ def dump(self):
+ """The dump() method returns a list of strings describing the memory
+ structure of the tag, suitable for printing with join(). The
+ list format makes custom indentation a bit easier. ::
+
+ print("\\n".join(["\\t" + line for line in tag.dump()]))
+
+ """
+ return []
+
+ def format(self, version=None, wipe=None):
+ """Format the tag to make it NDEF compatible or erase content.
+
+ The :meth:`format` method is highly dependent on the tag type,
+ product and present status, for example a tag that has been
+ made read-only with lock bits can no longer be formatted or
+ erased.
+
+ :meth:`format` creates the management information defined by
+ the NFC Forum to describes the NDEF data area on the tag, this
+ is also called NDEF mapping. The mapping may differ between
+ versions of the tag specifications, the mapping to apply can
+ be specified with the *version* argument as an 8-bit integer
+ composed of a major version number in the most significant 4
+ bit and the minor version number in the least significant 4
+ bit. If *version* is not specified then the highest possible
+ mapping version is used.
+
+ If formatting of the tag is possible, the default behavior of
+ :meth:`format` is to update only the management information
+ required to make the tag appear as NDEF compatible and empty,
+ previously existing data could still be read. If existing data
+ shall be overwritten, the *wipe* argument can be set to an
+ 8-bit integer that will be written to all available bytes.
+
+ The :meth:`format` method returns :const:`True` if formatting
+ was successful, :const:`False` if it failed for some reason,
+ or :const:`None` if the present tag can not be formatted
+ either because the tag does not support formatting or it is
+ not implemented in nfcpy.
+
+ """
+ if hasattr(self, "_format"):
+ args = "version={0!r}, wipe={1!r}"
+ args = args.format(version, wipe)
+ log.debug("format({0})".format(args))
+ status = self._format(version, wipe)
+ if status is True:
+ self._ndef = None
+ return status
+ else:
+ log.debug("this tag can not be formatted with nfcpy")
+ return None
+
+ def protect(self, password=None, read_protect=False, protect_from=0):
+ """Protect a tag against future write or read access.
+
+ :meth:`protect` attempts to make a tag readonly for all
+ readers if *password* is :const:`None`, writeable only after
+ authentication if a *password* is provided, and readable only
+ after authentication if a *password* is provided and the
+ *read_protect* flag is set. The *password* must be a byte or
+ character sequence that provides sufficient key material for
+ the tag specific protect function (this is documented
+ separately for the individual tag types). As a special case,
+ if *password* is set to an empty string the :meth:`protect`
+ method uses a default manufacturer value if such is known.
+
+ The *protect_from* argument sets the first memory unit to be
+ protected. Memory units are tag type specific, for a Type 1 or
+ Type 2 Tag a memory unit is 4 byte, for a Type 3 Tag it is 16
+ byte, and for a Type 4 Tag it is the complete NDEF data area.
+
+ Note that the effect of protecting a tag without password can
+ normally not be reversed.
+
+ The return value of :meth:`protect` is either :const:`True` or
+ :const:`False` depending on whether the operation was
+ successful or not, or :const:`None` if the tag does not
+ support custom protection (or it is not implemented).
+
+ """
+ if hasattr(self, "_protect"):
+ args = "password={0!r}, read_protect={1!r}, protect_from={2!r}"
+ args = args.format(password, read_protect, protect_from)
+ log.debug("protect({0})".format(args))
+ status = self._protect(password, read_protect, protect_from)
+ if status is True:
+ self._ndef = None
+ return status
+ else:
+ log.error("this tag can not be protected with nfcpy")
+ return None
+
+ def authenticate(self, password):
+ """Authenticate a tag with a *password*.
+
+ A tag that was once protected with a password requires
+ authentication before write, potentially also read, operations
+ may be performed. The *password* must be the same as the
+ password provided to :meth:`protect`. The return value
+ indicates authentication success with :const:`True` or
+ :const:`False`. For a tag that does not support authentication
+ the return value is :const:`None`.
+
+ """
+ if hasattr(self, "_authenticate"):
+ args = "password={0!r}".format(password)
+ log.debug("authenticate({0})".format(args))
+ self._authenticated = self._authenticate(password)
+ if self._authenticated is True:
+ self._ndef = None
+ return self._authenticated
+ else:
+ log.error("this tag can not be authenticated with nfcpy")
+ return None
+
+
+TIMEOUT_ERROR = 0
+RECEIVE_ERROR = -1
+PROTOCOL_ERROR = -2
+
+
+class TagCommandError(Exception):
+ """The base class for exceptions that are raised when a tag command
+ has not returned the expected result or a a lower stack error was
+ raised.
+
+ The :attr:`errno` attribute holds a reason code for why the
+ command has failed. Error numbers greater than zero indicate a tag
+ type specific error from one of the exception classes derived from
+ :exc:`TagCommandError` (per tag type module). Error numbers below
+ and including zero indicate general errors::
+
+ nfc.tag.TIMEOUT_ERROR => unrecoverable timeout error
+ nfc.tag.RECEIVE_ERROR => unrecoverable transmission error
+ nfc.tag.PROTOCOL_ERROR => unrecoverable protocol error
+
+ The :exc:`TagCommandError` exception populates the *message*
+ attribute of the general exception class with the appropriate
+ error description.
+
+ """
+ errno_str = {
+ TIMEOUT_ERROR: "unrecoverable timeout error",
+ RECEIVE_ERROR: "unrecoverable transmission error",
+ PROTOCOL_ERROR: "unrecoverable protocol error",
+ }
+
+ def __init__(self, errno):
+ default = "tag command error {errno} (0x{errno:x})".format(errno=errno)
+ if errno > 0:
+ message = self.errno_str.get(errno, default)
+ else:
+ message = TagCommandError.errno_str.get(errno, default)
+ super(TagCommandError, self).__init__(message)
+ self._errno = errno
+
+ @property
+ def errno(self):
+ """Holds the error reason code."""
+ return self._errno
+
+ def __int__(self):
+ return self._errno
+
+
+def activate(clf, target):
+ import src.lib.nfc.clf
+ try:
+ log.debug("trying to activate {0}".format(target))
+ if target.brty.endswith('A'):
+ if target.sens_res[1] & 0x0F == 0x0C:
+ return activate_tt1(clf, target)
+ elif target.sel_res[0] >> 5 & 3 == 0:
+ return activate_tt2(clf, target)
+ elif target.sel_res[0] >> 5 & 1 == 1:
+ return activate_tt4(clf, target)
+ elif target.brty.endswith('B'):
+ return activate_tt4(clf, target)
+ elif target.brty.endswith('F'):
+ return activate_tt3(clf, target)
+ except src.lib.nfc.clf.CommunicationError:
+ return None
+
+
+def activate_tt1(clf, target):
+ log.debug("trying type 1 tag activation for {0}".format(target.brty))
+ import src.lib.nfc.tag.tt1
+ return src.lib.nfc.tag.tt1.activate(clf, target)
+
+
+def activate_tt2(clf, target):
+ log.debug("trying type 2 tag activation for {0}".format(target.brty))
+ import src.lib.nfc.tag.tt2
+ return src.lib.nfc.tag.tt2.activate(clf, target)
+
+
+def activate_tt3(clf, target):
+ log.debug("trying type 3 tag activation for {0}".format(target.brty))
+ import src.lib.nfc.tag.tt3
+ return src.lib.nfc.tag.tt3.activate(clf, target)
+
+
+def activate_tt4(clf, target):
+ log.debug("trying type 4 tag activation for {0}".format(target.brty))
+ import src.lib.nfc.tag.tt4
+ return src.lib.nfc.tag.tt4.activate(clf, target)
+
+
+class TagEmulation(object):
+ """Base class for tag emulation classes."""
+ pass
+
+
+def emulate(clf, target):
+ import src.lib.nfc.clf
+ assert isinstance(target, src.lib.nfc.clf.LocalTarget)
+ if target.tt3_cmd:
+ import src.lib.nfc.tag.tt3
+ return src.lib.nfc.tag.tt3.Type3TagEmulation(clf, target)
+ else:
+ log.debug("can't emulate with %s", target)
diff --git a/src/lib/nfc/tag/tt1.py b/src/lib/nfc/tag/tt1.py
new file mode 100644
index 0000000..3e77996
--- /dev/null
+++ b/src/lib/nfc/tag/tt1.py
@@ -0,0 +1,555 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2011, 2017
+# Stephen Tiedemann
+# Alexander Knaub
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import time
+from binascii import hexlify
+from struct import pack, unpack
+
+from . import Tag, TagCommandError
+import nfc.clf
+
+import logging
+log = logging.getLogger(__name__)
+
+
+CHECKSUM_ERROR, RESPONSE_ERROR, WRITE_ERROR, \
+ BLOCK_ERROR, SECTOR_ERROR = range(1, 6)
+
+
+class Type1TagCommandError(TagCommandError):
+ """Type 1 Tag specific exceptions. Sets
+ :attr:`~nfc.tag.TagCommandError.errno` to one of:
+
+ | 1 - CHECKSUM_ERROR
+ | 2 - RESPONSE_ERROR
+ | 3 - WRITE_ERROR
+
+ """
+ errno_str = {
+ CHECKSUM_ERROR: "crc validation failed",
+ RESPONSE_ERROR: "invalid response data",
+ WRITE_ERROR: "data write failure",
+ }
+
+
+def read_tlv(memory, offset, skip_bytes):
+ # Unpack a TLV from tag memory and return tag type, tag length and
+ # tag value. For tag type 0 there is no length field, this is
+ # returned as length -1. The tlv length field can be one or three
+ # bytes, if the first byte is 255 then the next two byte carry the
+ # length (big endian).
+ try:
+ tlv_t, offset = (memory[offset], offset+1)
+ except Type1TagCommandError:
+ return (None, None, None)
+
+ if tlv_t in (0x00, 0xFE):
+ return (tlv_t, -1, None)
+
+ tlv_l, offset = (memory[offset], offset+1)
+
+ if tlv_l == 0xFF:
+ tlv_l, offset = (unpack(">H", memory[offset:offset+2])[0], offset+2)
+
+ tlv_v = bytearray(tlv_l)
+ for i in range(tlv_l):
+ while (offset + i) in skip_bytes:
+ offset += 1
+ tlv_v[i] = memory[offset+i]
+
+ return (tlv_t, tlv_l, tlv_v)
+
+
+def get_lock_byte_range(data):
+ # Extract the lock byte range indicated by a Lock Control TLV. The
+ # data argument is the TLV value field.
+ page_addr = data[0] >> 4
+ byte_offs = data[0] & 0x0F
+ rsvd_size = ((data[1] if data[1] > 0 else 256) + 7) // 8
+ page_size = 2 ** (data[2] & 0x0F)
+ rsvd_from = page_addr * page_size + byte_offs
+ return slice(rsvd_from, rsvd_from + rsvd_size)
+
+
+def get_rsvd_byte_range(data):
+ # Extract the reserved memory range indicated by a Memory Control
+ # TLV. The data argument is the TLV value field.
+ page_addr = data[0] >> 4
+ byte_offs = data[0] & 0x0F
+ rsvd_size = data[1] if data[1] > 0 else 256
+ page_size = 2 ** (data[2] & 0x0F)
+ rsvd_from = page_addr * page_size + byte_offs
+ return slice(rsvd_from, rsvd_from + rsvd_size)
+
+
+def get_capacity(tag_memory_size, offset, skip_bytes):
+ # The net capacity is the range of bytes from the current offset
+ # until the end of user data bytes (given by the capability
+ # container capacity value plus 16 header bytes), reduced by the
+ # number of skip bytes (from memory and lock control TLVs) that
+ # are within the usable memory range, and adjusted by the required
+ # number of TLV length bytes (1 or 3) and the TLV tag byte.
+ log.debug("subtract {0} skip bytes from capacity".format(len(skip_bytes)))
+ capacity = len(set(range(offset, tag_memory_size)) - skip_bytes)
+ # To store more than 254 byte ndef we must use three length bytes,
+ # otherwise it's only one. But only if the capacity is more than
+ # 256 the three length byte format will provide a higher value.
+ capacity -= 4 if capacity > 256 else 2
+ return capacity
+
+
+class Type1Tag(Tag):
+ """Implementation of the NFC Forum Type 1 Tag Operation specification.
+
+ The NFC Forum Type 1 Tag is based on the ISO 14443 Type A
+ technology for frame structure and anticollision (detection)
+ commands, and the Innovision (now Broadcom) Jewel/Topaz commands
+ for accessing the tag memory.
+
+ """
+ TYPE = "Type1Tag"
+
+ class NDEF(Tag.NDEF):
+ # Type 1 Tag specific implementation of the NDEF access type
+ # class that is returned by the Tag.ndef attribute.
+
+ def __init__(self, tag):
+ super(Type1Tag.NDEF, self).__init__(tag)
+ self._ndef_tlv_offset = 0
+
+ def _read_ndef_data(self):
+ # Check and read ndef data from tag. Return None if the
+ # tag is not ndef formatted, i.e. it can not hold ndef
+ # data or does not have (valid) ndef management data.
+ # Otherwise, set state variables and return the ndef
+ # message data as a bytearray (may be zero length).
+ log.debug("read ndef data")
+ try:
+ tag_memory = Type1TagMemoryReader(self.tag)
+
+ if tag_memory._header_rom[0] >> 4 != 1:
+ log.debug("proprietary type 1 tag memory structure")
+ return None
+
+ if tag_memory[8] != 0xE1:
+ log.debug("ndef management data is not present")
+ return None
+
+ if tag_memory[9] >> 4 != 1:
+ log.debug("unsupported ndef mapping version")
+ return None
+
+ self._readable = bool(tag_memory[11] >> 4 == 0)
+ self._writeable = bool(tag_memory[11] & 0xF == 0)
+
+ tag_memory_size = (tag_memory[10] + 1) * 8
+ log.debug("tag memory size is %d byte" % tag_memory_size)
+ except Type1TagCommandError:
+ log.debug("header rom and static memory were unreadable")
+ return None
+
+ ndef = None
+ offset = 12
+ skip_end = 120 if tag_memory_size == 120 else 128
+ skip_bytes = set(range(104, skip_end))
+ while offset < tag_memory_size:
+ if offset in skip_bytes:
+ offset += 1
+ continue
+
+ tlv_t, tlv_l, tlv_v = read_tlv(tag_memory, offset, skip_bytes)
+ log.debug("tlv type {0} at address {1}".format(tlv_t, offset))
+
+ if tlv_t == 0x00:
+ pass
+ elif tlv_t == 0x01:
+ lock_bytes = get_lock_byte_range(tlv_v)
+ skip_bytes.update(range(*lock_bytes.indices(0x800)))
+ elif tlv_t == 0x02:
+ rsvd_bytes = get_rsvd_byte_range(tlv_v)
+ skip_bytes.update(range(*rsvd_bytes.indices(0x800)))
+ elif tlv_t == 0x03:
+ ndef = tlv_v
+ break
+ elif tlv_t == 0xFE or tlv_t is None:
+ break
+ else:
+ logmsg = "unknown tlv {0} at offset {0}"
+ log.debug(logmsg.format(tlv_t, offset))
+
+ offset += tlv_l + 1 + (1 if tlv_l < 255 else 3)
+
+ self._capacity = get_capacity(tag_memory_size, offset, skip_bytes)
+ self._ndef_tlv_offset = offset
+ self._tag_memory = tag_memory
+ self._skip_bytes = skip_bytes
+ return ndef
+
+ def _write_ndef_data(self, data):
+ log.debug("write ndef data {0}{1}".format(
+ hexlify(data[:10]).decode(), '...' if len(data) > 10 else ''))
+
+ tag_memory = self._tag_memory
+ skip_bytes = self._skip_bytes
+ offset = self._ndef_tlv_offset
+ tag_memory_size = (tag_memory[10] + 1) * 8
+
+ # Set the ndef message tlv length to 0.
+ tag_memory[offset+1] = 0
+ tag_memory.synchronize()
+
+ # Leave room for ndef message length byte(s) and write
+ # ndef data into the memory image, but jump over skip
+ # bytes.
+ offset += 2 if len(data) < 255 else 4
+ for i in range(len(data)):
+ while offset + i in skip_bytes:
+ offset += 1
+ tag_memory[offset+i] = data[i]
+ # Write a terminator tlv if space permits. We may have to
+ # skip reserved and lock bytes.
+ offset = offset + i + 1
+ while offset < tag_memory_size:
+ if offset not in skip_bytes:
+ tag_memory[offset] = 0xFE
+ break
+ offset += 1
+ # Write the new message data to the tag.
+ tag_memory.synchronize()
+
+ # Write the ndef message tlv length.
+ offset = self._ndef_tlv_offset
+ if len(data) < 255:
+ tag_memory[offset+1] = len(data)
+ else:
+ tag_memory[offset+1] = 0xFF
+ tag_memory[offset+2:offset+4] = pack(">H", len(data))
+ tag_memory.synchronize()
+
+ #
+ # Type1Tag methods and attributes
+ #
+ def __init__(self, clf, target):
+ super(Type1Tag, self).__init__(clf, target)
+ self._nfcid = self.uid = target.rid_res[2:6]
+
+ def dump(self):
+ """Returns the tag memory blocks as a list of formatted strings.
+
+ :meth:`dump` iterates over all tag memory blocks (8 bytes
+ each) from block zero until the physical end of memory and
+ produces a list of strings that is intended for line by line
+ printing. Multiple consecutive memory block of identical
+ content may be reduced to fewer lines of output, so the number
+ of lines returned does not necessarily correspond to the
+ number of memory blocks present.
+
+ .. warning:: For tags with more than 120 byte memory, the
+ dump() method first overwrites the data block to verify
+ that it is backed by physical memory, then restores the
+ original data. This is necessary because Type 1 Tags do
+ not indicate an error when reading beyond the physical
+ memory space. Be cautious to not remove a tag from the
+ reader when using dump() as otherwise your data may be
+ corrupted.
+
+ """
+ return self._dump(stop=None)
+
+ def _dump(self, stop=None):
+ # Read and print all data blocks until the non-inclusive stop
+ # block number. Type 1 Tags with dynamic memory seem to return
+ # data for every address, regardless of whether there is
+ # memory mapped or not. To show exactly the memory blocks that
+ # are physically present, blocks from 16-end are first
+ # overwritten with an inverted version of the content and then
+ # recovered. Because WRITE8 returns the new data content, a
+ # non-existing block can be detected.
+
+ def oprint(octets):
+ return ' '.join(['??' if x < 0 else '%02x' % x for x in octets])
+
+ def cprint(octets):
+ return ''.join([chr(x) if 32 <= x <= 126 else '.' for x in octets])
+
+ def lprint(fmt, d, i):
+ return fmt.format(i, oprint(d), cprint(d))
+
+ txt = ["UID0-UID6, RESERVED", "RESERVED", "LOCK0-LOCK1, OTP0-OTP5",
+ "LOCK2-LOCK3, RESERVED"]
+
+ lines = list()
+ data = self.read_all()
+ hrom, data = data[0:2], data[2:]
+
+ lines.append("HR0={0:02X}h, HR1={1:02X}h".format(*hrom))
+ lines.append(" 0: {0} ({1})".format(oprint(data[0:8]), txt[0]))
+ for i in range(8, 104, 8):
+ lines.append(lprint("{0:3}: {1} |{2}|", data[i:i+8], i//8))
+ lines.append(" 13: {0} ({1})".format(oprint(data[104:112]), txt[1]))
+ lines.append(" 14: {0} ({1})".format(oprint(data[112:120]), txt[2]))
+
+ if stop is None or stop > 15:
+ try:
+ data = self.read_block(15)
+ except Type1TagCommandError:
+ return lines
+ else:
+ lines.append(" 15: {0} ({1})".format(oprint(data), txt[3]))
+
+ data_line_fmt = "{0:>3}: {1} |{2}|"
+ same_line_fmt = "{0:>3} {1} |{2}|"
+ this_data = last_data = None
+ same_data = 0
+
+ def dump_same_data(same_data, last_data, this_data, page):
+ if same_data > 1:
+ lines.append(lprint(same_line_fmt, last_data, "*"))
+ if same_data > 0:
+ lines.append(lprint(data_line_fmt, this_data, page))
+
+ for i in range(16, stop if stop is not None else 256):
+ try:
+ this_data = self.read_block(i)
+ if stop is None:
+ test_data = bytearray([b ^ 0xFF for b in this_data])
+ self.write_block(i, test_data)
+ self.write_block(i, this_data)
+ except Type1TagCommandError:
+ dump_same_data(same_data, last_data, this_data, i-1)
+ break
+
+ if this_data == last_data:
+ same_data += 1
+ else:
+ dump_same_data(same_data, last_data, last_data, i-1)
+ lines.append(lprint(data_line_fmt, this_data, i))
+ last_data = this_data
+ same_data = 0
+ else:
+ dump_same_data(same_data, last_data, this_data, i)
+
+ return lines
+
+ def protect(self, password=None, read_protect=False, protect_from=0):
+ """The implementation of :meth:`nfc.tag.Tag.protect` for a generic
+ type 1 tag is limited to setting the NDEF data read-only for
+ tags that are already NDEF formatted.
+
+ """
+ return super(Type1Tag, self).protect(
+ password, read_protect, protect_from)
+
+ def _protect(self, password, read_protect, protect_from):
+ if password is None:
+ if self.ndef is not None:
+ self.write_byte(11, 0x0F, erase=False)
+ return True
+ else:
+ log.warning("no ndef, can't set write access restriction")
+ else:
+ log.warning("this tag can not be protected with a password")
+ return False
+
+ def _is_present(self):
+ try:
+ return self.read_byte(0) == self.uid[0]
+ except Type1TagCommandError:
+ return False
+
+ def read_id(self):
+ """Returns the 2 byte Header ROM and 4 byte UID.
+ """
+ log.debug("read identification")
+ cmd = b"\x78\x00\x00\x00\x00\x00\x00"
+ return self.transceive(cmd)
+
+ def read_all(self):
+ """Returns the 2 byte Header ROM and all 120 byte static memory.
+ """
+ log.debug("read all static memory")
+ cmd = b"\x00\x00\x00" + self.uid
+ return self.transceive(cmd)
+
+ def read_byte(self, addr):
+ """Read a single byte from static memory area (blocks 0-14).
+ """
+ if addr < 0 or addr > 127:
+ raise ValueError("invalid byte address")
+ log.debug("read byte at address {0} ({0:02X}h)".format(addr))
+ cmd = bytearray([0x01, addr, 0x00]) + self.uid
+ return self.transceive(cmd)[-1]
+
+ def read_block(self, block):
+ """Read an 8-byte data block at address (block * 8).
+ """
+ if block < 0 or block > 255:
+ raise ValueError("invalid block number")
+ log.debug("read block {0}".format(block))
+ cmd = bytearray([0x02, block] + [0x00 for _ in range(8)]) + self.uid
+ return self.transceive(cmd)[1:9]
+
+ def read_segment(self, segment):
+ """Read one memory segment (128 byte).
+ """
+ log.debug("read segment {0}".format(segment))
+ if segment < 0 or segment > 15:
+ raise ValueError("invalid segment number")
+ cmd = bytearray([0x10, segment << 4] + [0x00 for _ in range(8)]) \
+ + self.uid
+ rsp = self.transceive(cmd)
+ if len(rsp) < 129:
+ raise Type1TagCommandError(RESPONSE_ERROR)
+ return rsp[1:129]
+
+ def write_byte(self, addr, data, erase=True):
+ """Write a single byte to static memory area (blocks 0-14). The
+ target byte is zero'd first if *erase* is True.
+
+ """
+ if addr < 0 or addr >= 128:
+ raise ValueError("invalid byte address")
+ log.debug("write byte at address {0} ({0:02X}h)".format(addr))
+ cmd = b"\x53" if erase is True else b"\x1A"
+ cmd = cmd + bytearray([addr, data]) + self.uid
+ return self.transceive(cmd)
+
+ def write_block(self, block, data, erase=True):
+ """Write an 8-byte data block at address (block * 8). The target
+ bytes are zero'd first if *erase* is True.
+
+ """
+ if block < 0 or block > 255:
+ raise ValueError("invalid block number")
+ log.debug("write block {0}".format(block))
+ cmd = b"\x54" if erase is True else b"\x1B"
+ cmd = cmd + bytearray([block]) + data + self.uid
+ rsp = self.transceive(cmd)
+ if len(rsp) < 9:
+ raise Type1TagCommandError(RESPONSE_ERROR)
+ if erase is True and rsp[1:9] != data:
+ raise Type1TagCommandError(WRITE_ERROR)
+
+ def transceive(self, data, timeout=0.1):
+ log.debug(">> {0} ({1:f}s)".format(hexlify(data).decode(), timeout))
+
+ started = time.time()
+ error = None
+ for retry in range(3):
+ try:
+ data = self.clf.exchange(data, timeout)
+ break
+ except nfc.clf.CommunicationError as e:
+ error = e
+ reason = error.__class__.__name__
+ log.debug("%s after %d retries" % (reason, retry))
+ else:
+ if type(error) is nfc.clf.TimeoutError:
+ raise Type1TagCommandError(nfc.tag.TIMEOUT_ERROR)
+ if type(error) is nfc.clf.TransmissionError:
+ raise Type1TagCommandError(nfc.tag.RECEIVE_ERROR)
+ if type(error) is nfc.clf.ProtocolError:
+ raise Type1TagCommandError(nfc.tag.PROTOCOL_ERROR)
+ raise RuntimeError("unexpected " + repr(error))
+
+ elapsed = time.time() - started
+ log.debug("<< {0} ({1:f}s)".format(hexlify(data).decode(), elapsed))
+ return data
+
+
+class Type1TagMemoryReader(object):
+ def __init__(self, tag):
+ assert isinstance(tag, Type1Tag)
+ self._data_from_tag = bytearray()
+ self._data_in_cache = bytearray()
+ self._tag = tag
+ self._header_rom = bytearray(0)
+ # read header_rom and static memory
+ self._read_from_tag(1)
+
+ def __len__(self):
+ return len(self._data_from_tag)
+
+ def __getitem__(self, key):
+ if isinstance(key, slice):
+ start, stop, step = key.indices(0x100000)
+ if stop > len(self):
+ self._read_from_tag(stop)
+ elif key >= len(self):
+ self._read_from_tag(stop=key+1)
+ return self._data_in_cache[key]
+
+ def __setitem__(self, key, value):
+ self.__getitem__(key)
+ if isinstance(key, slice):
+ if len(value) != len(range(*key.indices(0x100000))):
+ msg = "{cls} requires item assignment of identical length"
+ raise ValueError(msg.format(cls=self.__class__.__name__))
+ self._data_in_cache[key] = value
+ del self._data_in_cache[len(self):]
+
+ def __delitem__(self, key):
+ msg = "{cls} object does not support item deletion"
+ raise TypeError(msg.format(cls=self.__class__.__name__))
+
+ def _read_from_tag(self, stop):
+ if len(self) < 120:
+ read_all_data_response = self._tag.read_all()
+ self._header_rom = read_all_data_response[0:2]
+ self._data_from_tag[0:] = read_all_data_response[2:]
+ self._data_in_cache[0:] = self._data_from_tag[0:]
+
+ if stop > 120 and len(self) < 128:
+ read_block_response = self._tag.read_block(15)
+ self._data_from_tag[120:128] = read_block_response
+ self._data_in_cache[120:128] = read_block_response
+
+ while len(self) < stop:
+ data = self._tag.read_segment(len(self) >> 7)
+ self._data_from_tag.extend(data)
+ self._data_in_cache.extend(data)
+
+ def _write_to_tag(self, stop):
+ hr0 = self._header_rom[0]
+ if hr0 >> 4 == 1 and hr0 & 0x0F != 1:
+ for i in range(0, stop, 8):
+ data = self._data_in_cache[i:i+8]
+ if data != self._data_from_tag[i:i+8]:
+ self._tag.write_block(i//8, data)
+ self._data_from_tag[i:i+8] = data
+ else:
+ for i in range(0, stop):
+ data = self._data_in_cache[i]
+ if data != self._data_from_tag[i]:
+ self._tag.write_byte(i, data)
+ self._data_from_tag[i] = data
+
+ def synchronize(self):
+ """Write pages that contain modified data back to tag memory."""
+ self._write_to_tag(stop=len(self))
+
+
+def activate(clf, target):
+ import nfc.tag.tt1_broadcom
+ tag = nfc.tag.tt1_broadcom.activate(clf, target)
+ return tag if tag is not None else Type1Tag(clf, target)
diff --git a/src/lib/nfc/tag/tt1_broadcom.py b/src/lib/nfc/tag/tt1_broadcom.py
new file mode 100644
index 0000000..7f700fe
--- /dev/null
+++ b/src/lib/nfc/tag/tt1_broadcom.py
@@ -0,0 +1,159 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2014, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+from . import tt1
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Topaz(tt1.Type1Tag):
+ """The Broadcom Topaz is a small memory tag that can hold up to 94
+ byte ndef message data.
+
+ """
+ def __init__(self, clf, target):
+ super(Topaz, self).__init__(clf, target)
+ self._product = "Topaz (BCM20203T96)"
+
+ def dump(self):
+ return super(Topaz, self)._dump(stop=15)
+
+ def format(self, version=None, wipe=None):
+ """Format a Topaz tag for NDEF use.
+
+ The implementation of :meth:`nfc.tag.Tag.format` for a Topaz
+ tag creates a capability container and an NDEF TLV with length
+ zero. Data bytes of the NDEF data area are left untouched
+ unless the wipe argument is set.
+
+ """
+ return super(Topaz, self).format(version, wipe)
+
+ def _format(self, version, wipe):
+ tag_memory = tt1.Type1TagMemoryReader(self)
+ tag_memory[8:14] = b"\xE1\x10\x0E\x00\x03\x00"
+
+ if version is not None:
+ if version >> 4 == 1:
+ tag_memory[9] = version
+ else:
+ log.warning("can not format with major version != 1")
+ return False
+
+ if wipe is not None:
+ tag_memory[14:104] = bytearray([wipe & 0xFF]) * 90
+
+ tag_memory.synchronize()
+ return True
+
+ def protect(self, password=None, read_protect=False, protect_from=0):
+ """In addtion to :meth:`nfc.tag.tt1.Type1Tag.protect` this method
+ tries to set the lock bits to irreversibly protect the tag
+ memory. However, it appears that tags sold have the lock bytes
+ write protected, so this additional effort most likely doesn't
+ have any effect.
+
+ """
+ return super(Topaz, self).protect(
+ password, read_protect, protect_from)
+
+ def _protect(self, password, read_protect, protect_from):
+ if super(Topaz, self)._protect(password, read_protect, protect_from):
+ self.write_byte(112, 0xFF, erase=False)
+ self.write_byte(113, 0xFF, erase=False)
+ return True
+ else:
+ return False
+
+
+class Topaz512(tt1.Type1Tag):
+ """The Broadcom Topaz-512 is a memory enhanced version that can hold
+ up to 462 byte ndef message data.
+
+ """
+ def __init__(self, clf, target):
+ super(Topaz512, self).__init__(clf, target)
+ self._product = "Topaz 512 (BCM20203T512)"
+
+ def dump(self):
+ return super(Topaz512, self)._dump(stop=64)
+
+ def format(self, version=None, wipe=None):
+ """Format a Topaz-512 tag for NDEF use.
+
+ The implementation of :meth:`nfc.tag.Tag.format` for a
+ Topaz-512 tag creates a capability container, a Lock Control
+ and a Memory Control TLV, and an NDEF TLV with length
+ zero. Data bytes of the NDEF data area are left untouched
+ unless the wipe argument is set.
+
+ """
+ return super(Topaz512, self).format(version, wipe)
+
+ def _format(self, version, wipe):
+ tag_memory = tt1.Type1TagMemoryReader(self)
+ tag_memory[8:16] = bytearray.fromhex("E1103F000103F230")
+ tag_memory[16:24] = bytearray.fromhex("330203F002030300")
+
+ if version is not None:
+ if version >> 4 == 1:
+ tag_memory[9] = version
+ else:
+ log.warning("can not format with major version != 1")
+ return False
+
+ if wipe is not None:
+ tag_memory[24:104] = bytearray([wipe & 0xFF]) * 80
+ tag_memory[128:512] = bytearray([wipe & 0xFF]) * 384
+
+ tag_memory.synchronize()
+ return True
+
+ def protect(self, password=None, read_protect=False, protect_from=0):
+ """In addtion to :meth:`nfc.tag.tt1.Type1Tag.protect` this method
+ tries to set the lock bits to irreversibly protect the tag
+ memory. However, it appears that tags sold have the lock bytes
+ write protected, so this additional effort most likely doesn't
+ have any effect.
+
+ """
+ return super(Topaz512, self).protect(
+ password, read_protect, protect_from)
+
+ def _protect(self, password, read_protect, protect_from):
+ if super(Topaz512, self)._protect(
+ password, read_protect, protect_from):
+ self.write_byte(112, 0xFF, erase=False)
+ self.write_byte(113, 0xFF, erase=False)
+ self.write_byte(120, 0xFF, erase=False)
+ self.write_byte(121, 0xFF, erase=False)
+ return True
+ else:
+ return False
+
+
+def activate(clf, target):
+ hrom = target.rid_res[0:2]
+ if hrom == b"\x11\x48":
+ return Topaz(clf, target)
+ if hrom == b"\x12\x4C":
+ return Topaz512(clf, target)
diff --git a/src/lib/nfc/tag/tt2.py b/src/lib/nfc/tag/tt2.py
new file mode 100644
index 0000000..52c3f93
--- /dev/null
+++ b/src/lib/nfc/tag/tt2.py
@@ -0,0 +1,697 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# You may obtain a copy of the Licence at:
+#
+
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import time
+from binascii import hexlify
+from struct import pack, unpack
+
+from . import Tag, TagCommandError
+import src.lib.nfc.clf
+
+import logging
+log = logging.getLogger(__name__)
+
+
+def hexdump(octets, sep=""):
+ return sep.join(
+ ("??" if x is None else ("%02x" % x)) for x in octets)
+
+
+def chrdump(octets, sep=""):
+ return sep.join(
+ (("{:c}".format(x) if 32 <= x <= 126 else ".")
+ if x is not None
+ else ".")
+ for x in octets)
+
+
+def pagedump(page, octets, info=None):
+ info = ("|%s|" % chrdump(octets)) if info is None else ("(%s)" % info)
+ page = " * " if page is None else "{0:03X}:".format(page)
+ return "{0} {1} {2}".format(page, hexdump(octets, sep=" "), info)
+
+
+TIMEOUT_ERROR, INVALID_SECTOR_ERROR, \
+ INVALID_PAGE_ERROR, INVALID_RESPONSE_ERROR = range(4)
+
+
+class Type2TagCommandError(TagCommandError):
+ """Type 2 Tag specific exceptions. Sets
+ :attr:`~nfc.tag.TagCommandError.errno` to one of:
+
+ | 1 - INVALID_SECTOR_ERROR
+ | 2 - INVALID_PAGE_ERROR
+ | 3 - INVALID_RESPONSE_ERROR
+
+ """
+ errno_str = {
+ INVALID_SECTOR_ERROR: "invalid sector number",
+ INVALID_PAGE_ERROR: "invalid page number",
+ INVALID_RESPONSE_ERROR: "invalid response data",
+ }
+
+
+def read_tlv(memory, offset, skip_bytes):
+ # Unpack a Type 2 Tag TLV from tag memory and return tag type, tag
+ # length and tag value. For tag type 0 there is no length field,
+ # this is returned as length -1. The tlv length field can be one
+ # or three bytes, if the first byte is 255 then the next two byte
+ # carry the length (big endian).
+ tlv_t, offset = (memory[offset], offset+1)
+ if tlv_t in (0x00, 0xFE):
+ return (tlv_t, -1, None)
+ tlv_l, offset = (memory[offset], offset+1)
+ if tlv_l == 0xFF:
+ tlv_l, offset = (unpack(">H", memory[offset:offset+2])[0], offset+2)
+ tlv_v = bytearray(tlv_l)
+ for i in range(tlv_l):
+ while (offset + i) in skip_bytes:
+ offset += 1
+ tlv_v[i] = memory[offset+i]
+ return (tlv_t, tlv_l, tlv_v)
+
+
+def get_lock_byte_range(data):
+ # Extract the lock byte range indicated by a Lock Control TLV. The
+ # data argument is the TLV value field.
+ page_addr = data[0] >> 4
+ byte_offs = data[0] & 0x0F
+ rsvd_size = ((data[1] if data[1] > 0 else 256) + 7) // 8
+ page_size = 2 ** (data[2] & 0x0F)
+ rsvd_from = page_addr * page_size + byte_offs
+ return slice(rsvd_from, rsvd_from + rsvd_size)
+
+
+def get_rsvd_byte_range(data):
+ # Extract the reserved memory range indicated by a Memory Control
+ # TLV. The data argument is the TLV value field.
+ page_addr = data[0] >> 4
+ byte_offs = data[0] & 0x0F
+ rsvd_size = data[1] if data[1] > 0 else 256
+ page_size = 2 ** (data[2] & 0x0F)
+ rsvd_from = page_addr * page_size + byte_offs
+ return slice(rsvd_from, rsvd_from + rsvd_size)
+
+
+def get_capacity(capacity, offset, skip_bytes):
+ # The net capacity is the range of bytes from the current offset
+ # until the end of user data bytes (given by the capability
+ # container capacity value plus 16 header bytes), reduced by the
+ # number of skip bytes (from memory and lock control TLVs) that
+ # are within the usable memory range, and adjusted by the required
+ # number of TLV length bytes (1 or 3) and the TLV tag byte.
+ capacity = len(set(range(offset, capacity + 16)) - skip_bytes)
+ # To store more than 254 byte ndef we must use three length bytes,
+ # otherwise it's only one. But only if the capacity is more than
+ # 256 the three length byte format will provide a higher value.
+ capacity -= 4 if capacity > 256 else 2
+ return capacity
+
+
+class Type2Tag(Tag):
+ """Implementation of the NFC Forum Type 2 Tag Operation specification.
+
+ The NFC Forum Type 2 Tag is based on the ISO 14443 Type A
+ technology for frame structure and anticollision (detection)
+ commands, and the NXP Mifare commands for accessing the tag
+ memory.
+
+ """
+ TYPE = "Type2Tag"
+
+ class NDEF(Tag.NDEF):
+ # Type 2 Tag specific implementation of the NDEF access type
+ # class that is returned by the Tag.ndef attribute.
+
+ def __init__(self, tag):
+ super(Type2Tag.NDEF, self).__init__(tag)
+ self._ndef_tlv_offset = 0
+
+ def _read_capability_data(self, tag_memory):
+ try:
+ if tag_memory[12] != 0xE1:
+ log.debug("ndef management data is not present")
+ return False
+ if tag_memory[13] >> 4 != 1:
+ log.debug("unsupported ndef mapping major version")
+ return False
+ self._readable = bool(tag_memory[15] >> 4 == 0)
+ self._writeable = bool(tag_memory[15] & 0xF == 0)
+ return True
+ except Type2TagCommandError:
+ log.debug("first four memory pages were unreadable")
+ return False
+
+ def _read_ndef_data(self):
+ log.debug("read ndef data")
+ tag_memory = Type2TagMemoryReader(self.tag)
+
+ if not self._read_capability_data(tag_memory):
+ return None
+
+ raw_capacity = tag_memory[14] * 8
+ log.debug("raw capacity is {0} byte".format(raw_capacity))
+
+ offset = 16
+ ndef = None
+ skip_bytes = set()
+ data_area_size = raw_capacity
+ while offset < data_area_size + 16:
+ while (offset) in skip_bytes:
+ offset += 1
+
+ try:
+ tlv = read_tlv(tag_memory, offset, skip_bytes)
+ tlv_t, tlv_l, tlv_v = tlv
+ except Type2TagCommandError:
+ return None
+ else:
+ logmsg = "tlv type {0} length {1} at offset {2}"
+ log.debug(logmsg.format(tlv_t, tlv_l, offset))
+
+ if tlv_t == 0:
+ pass
+ elif tlv_t == 1:
+ if tlv_l == 3:
+ lock_bytes = get_lock_byte_range(tlv_v)
+ skip_bytes.update(range(*lock_bytes.indices(0x100000)))
+ else:
+ log.debug("lock tlv has wrong length")
+ elif tlv_t == 2:
+ if tlv_l == 3:
+ rsvd_bytes = get_rsvd_byte_range(tlv_v)
+ skip_bytes.update(range(*rsvd_bytes.indices(0x100000)))
+ else:
+ log.debug("memory tlv has wrong length")
+ elif tlv_t == 3:
+ ndef = tlv_v
+ break
+ elif tlv_t == 254:
+ break
+ else:
+ logmsg = "unknown tlv {0} at offset {0}"
+ log.debug(logmsg.format(tlv_t, offset))
+
+ offset += tlv_l + 1 + (1 if tlv_l < 255 else 3)
+
+ self._capacity = get_capacity(raw_capacity, offset, skip_bytes)
+ self._ndef_tlv_offset = offset
+ self._tag_memory = tag_memory
+ self._skip_bytes = skip_bytes
+ return ndef
+
+ def _write_ndef_data(self, data):
+ # Write new ndef data to the tag memory. Despite the
+ # tag memory is rather easy to handle, the extremely
+ # generic NFC Forum TLV structure makes this rather
+ # complicated. The precondition is that we have already
+ # processed the memory structure in _read_ndef_data(), if
+ # not we'll do it first. We'll then have a tag memory
+ # image, know which bytes need to be to skipped as told by
+ # memory or control tlv data, and where the ndef message
+ # tlv starts. We first set the ndef message tlv length to
+ # zero (synchronize cause that to be actually written),
+ # then write all new data into the memory image (skipping
+ # bytes as needed) and let that be written to the tag, and
+ # finally write the new ndef message tlv length.
+ log.debug("write ndef data {0}{1}".format(
+ hexlify(data[:10]).decode(), '...' if len(data) > 10 else ''))
+
+ tag_memory = self._tag_memory
+ skip_bytes = self._skip_bytes
+ offset = self._ndef_tlv_offset
+
+ # Set the ndef message tlv length to 0.
+ tag_memory[offset+1] = 0
+ tag_memory.synchronize()
+
+ # Leave room for ndef message length byte(s) and write
+ # ndef data into the memory image, but jump over skip
+ # bytes. If space permits, write a terminator tlv.
+ offset += 2 if len(data) < 255 else 4
+ for index, octet in enumerate(data):
+ while offset + index in skip_bytes:
+ offset += 1
+ tag_memory[offset+index] = octet
+ offset = offset + index + 1
+ while offset in skip_bytes:
+ offset += 1
+ if offset < tag_memory[14] * 8 + 16:
+ tag_memory[offset] = 0xFE
+ tag_memory.synchronize()
+
+ # Write the ndef message tlv length.
+ offset = self._ndef_tlv_offset
+ if len(data) < 255:
+ tag_memory[offset+1] = len(data)
+ else:
+ tag_memory[offset+1] = 0xFF
+ tag_memory[offset+2:offset+4] = pack(">H", len(data))
+ tag_memory.synchronize()
+
+ #
+ # Type2Tag methods and attributes
+ #
+ def __init__(self, clf, target):
+ super(Type2Tag, self).__init__(clf, target)
+ self._nfcid = bytearray(target.sdd_res)
+ self._current_sector = 0
+
+ def dump(self):
+ """Returns the tag memory pages as a list of formatted strings.
+
+ :meth:`dump` iterates over all tag memory pages (4 bytes
+ each) from page zero until an error response is received and
+ produces a list of strings that is intended for line by line
+ printing. Note that multiple consecutive memory pages of
+ identical content may be reduced to fewer lines of output, so
+ the number of lines returned does not necessarily correspond
+ to the number of memory pages.
+
+ """
+ return self._dump(stop=None)
+
+ def _dump(self, stop=None):
+ lines = list()
+ header = ("UID0-UID2, BCC0", "UID3-UID6",
+ "BCC1, INT, LOCK0-LOCK1", "OTP0-OTP3")
+
+ for i, info in enumerate(header):
+ try:
+ data = self.read(i)[0:4]
+ except Type2TagCommandError:
+ data = [None, None, None, None]
+ lines.append(pagedump(i, data, info))
+
+ this_data = last_data = None
+ same_data = 0
+
+ def dump_same_data(same_data, last_data, this_data, page):
+ if same_data > 1:
+ lines.append(pagedump(None, this_data))
+ if same_data > 0:
+ lines.append(pagedump(page, this_data))
+
+ for i in range(4, stop if stop is not None else 0x40000):
+ try:
+ self.sector_select(i >> 8)
+ this_data = self.read(i)[0:4]
+ except Type2TagCommandError:
+ dump_same_data(same_data, last_data, this_data, i-1)
+ if stop is not None:
+ this_data = last_data = [None, None, None, None]
+ lines.append(pagedump(i, this_data))
+ dump_same_data(stop-i-1, this_data, this_data, stop-1)
+ break
+
+ if this_data == last_data:
+ same_data += 1
+ else:
+ dump_same_data(same_data, last_data, last_data, i-1)
+ lines.append(pagedump(i, this_data))
+ last_data = this_data
+ same_data = 0
+ else:
+ dump_same_data(same_data, last_data, this_data, i)
+
+ return lines
+
+ def _is_present(self):
+ # Verify that the tag is still present. This is implemented as
+ # reading page 0-3 (from whatever sector is currently active).
+ try:
+ data = self.transceive(b"\x30\x00")
+ except Type2TagCommandError as error:
+ if error.errno != TIMEOUT_ERROR:
+ log.warning("unexpected error in presence check: %s" % error)
+ return False
+ else:
+ return bool(data and len(data) == 16)
+
+ def format(self, version=None, wipe=None):
+ """Erase the NDEF message on a Type 2 Tag.
+
+ The :meth:`format` method will reset the length of the NDEF
+ message on a type 2 tag to zero, thus the tag will appear to
+ be empty. Additionally, if the *wipe* argument is set to some
+ integer then :meth:`format` will overwrite all user date that
+ follows the NDEF message TLV with that integer (mod 256). If
+ an NDEF message TLV is not present it will be created with a
+ length of zero.
+
+ Despite it's name, the :meth:`format` method can not format a
+ blank tag to make it NDEF compatible. This is because the user
+ data are of a type 2 tag can not be safely determined, also
+ reading all memory pages until an error response yields only
+ the total memory size which includes an undetermined number of
+ special pages at the end of memory.
+
+ It is also not possible to change the NDEF mapping version,
+ located in a one-time-programmable area of the tag memory.
+
+ """
+ return super(Type2Tag, self).format(version, wipe)
+
+ def _format(self, version, wipe):
+ if self.ndef and self.ndef.is_writeable:
+ memory = self.ndef._tag_memory
+ offset = self.ndef._ndef_tlv_offset
+ memory[offset+1:offset+3] = b"\x00\xFE"
+ if wipe is not None:
+ memory_size = memory[14] * 8 + 16
+ skip_bytes = self.ndef._skip_bytes
+ for offset in range(offset + 3, memory_size):
+ if offset not in skip_bytes:
+ memory[offset] = wipe & 0xFF
+ memory.synchronize()
+ return True
+ return False
+
+ def protect(self, password=None, read_protect=False, protect_from=0):
+ """Protect the tag against write access, i.e. make it read-only.
+
+ :meth:`Type2Tag.protect` switches an NFC Forum Type 2 Tag to
+ read-only state by setting all lock bits to 1. This operation
+ can not be reversed. If the tag is not an NFC Forum Tag,
+ i.e. it is not formatted with an NDEF Capability Container,
+ the :meth:`protect` method simply returns :const:`False`.
+
+ A generic Type 2 Tag can not be protected with a password. If
+ the *password* argument is provided, the :meth:`protect`
+ method does nothing else than return :const:`False`. The
+ *read_protect* and *protect_from* arguments are safely
+ ignored.
+
+ """
+ return super(Type2Tag, self).protect(
+ password, read_protect, protect_from)
+
+ def _protect(self, password, read_protect, protect_from):
+ if password is not None:
+ log.debug("this tag can not be protected with password")
+ return False
+
+ if self.ndef is None:
+ log.debug("can not protect a non-ndef tag")
+ return False
+
+ # Set the ndef capability container write flag. We must
+ # synchronize to have this written before lock bits are set.
+ tag_memory = self.ndef._tag_memory
+ tag_memory[15] |= 0x0F
+ tag_memory.synchronize()
+
+ # Set the static lock bits.
+ tag_memory[10] = 0xFF
+ tag_memory[11] = 0xFF
+
+ # Search for all lock control tlv and store the first lock
+ # byte address and the number of lock bits in lock_control.
+ offset = 16
+ lock_control = []
+ data_area_size = tag_memory[14] * 8
+ while offset < data_area_size + 16: # pragma: no branch
+ tlv_t, tlv_l, tlv_v = read_tlv(tag_memory, offset, set())
+ log.debug("tlv type {0} at offset {1}".format(tlv_t, offset))
+ if tlv_t in (0x03, 0xFE, None):
+ break
+ if tlv_t == 0x01:
+ log.debug("lock control tlv %s", hexlify(tlv_v).decode())
+ page_addr = tlv_v[0] >> 4
+ byte_offs = tlv_v[0] & 0x0F
+ page_size = 2 ** (tlv_v[2] & 0x0F) # BytesPerPage
+ lock_byte_addr = page_addr * page_size + byte_offs
+ lock_bits_size = tlv_v[1] if tlv_v[1] > 0 else 256
+ lock_control.append((lock_byte_addr, lock_bits_size))
+ offset += tlv_l + 1 + (1 if tlv_l < 255 else 3)
+
+ # If the tag has a dynamic memory layout and we did not find
+ # any lock control tlv, then add default dynamic lock bits.
+ if tag_memory[14] > 6 and len(lock_control) == 0:
+ # use default dynamic lock bits layout
+ data_area_size = tag_memory[14] * 8
+ lock_byte_addr = 16 + data_area_size
+ lock_bits_size = (data_area_size - 48 + 7)//8
+ lock_control.append((lock_byte_addr, lock_bits_size))
+
+ # For any lock control entry set the referenced lock bytes to
+ # zero and then set the lock bits to one.
+ log.debug("processing lock byte list {0}".format(lock_control))
+ for lock_byte_addr, lock_bits_size in lock_control:
+ log.debug("{0} lock bits at 0x{1:02x}".format(
+ lock_bits_size, lock_byte_addr))
+ lock_byte_size = (lock_bits_size + 7) // 8
+ for i in range(lock_byte_size):
+ tag_memory[lock_byte_addr+i] = 0
+ for i in range(lock_bits_size):
+ tag_memory[lock_byte_addr+(i >> 3)] |= 1 << (i & 7)
+
+ # Synchronize to write all lock bits to the tag.
+ tag_memory.synchronize()
+ return True
+
+ def read(self, page):
+ """Send a READ command to retrieve data from the tag.
+
+ The *page* argument specifies the offset in multiples of 4
+ bytes (i.e. page number 1 will return bytes 4 to 19). The data
+ returned is a byte array of length 16 or None if the block is
+ outside the readable memory range.
+
+ Command execution errors raise :exc:`Type2TagCommandError`.
+
+ """
+ log.debug("read pages {0} to {1}".format(page, page+3))
+
+ data = self.transceive(bytearray([0x30, page % 256]), timeout=0.005)
+
+ if len(data) == 1 and data[0] & 0xFA == 0x00:
+ log.debug("received nak response")
+ self.target.sel_req = self.target.sdd_res[:]
+ self._target = self.clf.sense(self.target)
+ raise Type2TagCommandError(
+ INVALID_PAGE_ERROR if self.target else src.lib.nfc.tag.RECEIVE_ERROR)
+
+ if len(data) != 16:
+ log.debug("invalid response %s", hexlify(data).decode())
+ raise Type2TagCommandError(INVALID_RESPONSE_ERROR)
+
+ return data
+
+ def write(self, page, data):
+ """Send a WRITE command to store data on the tag.
+
+ The *page* argument specifies the offset in multiples of 4
+ bytes. The *data* argument must be a string or bytearray of
+ length 4.
+
+ Command execution errors raise :exc:`Type2TagCommandError`.
+
+ """
+ if len(data) != 4:
+ raise ValueError("data must be a four byte string or array")
+
+ log.debug("write %s to page %s", hexlify(data).decode(), page)
+ rsp = self.transceive(bytearray([0xA2, page % 256]) + data)
+
+ if len(rsp) != 1:
+ log.debug("invalid response %s", hexlify(data).decode())
+ raise Type2TagCommandError(INVALID_RESPONSE_ERROR)
+
+ if rsp[0] != 0x0A: # NAK
+ log.debug("invalid page, received nak")
+ raise Type2TagCommandError(INVALID_PAGE_ERROR)
+
+ return True
+
+ def sector_select(self, sector):
+ """Send a SECTOR_SELECT command to switch the 1K address sector.
+
+ The command is only send to the tag if the *sector* number is
+ different from the currently selected sector number (set to 0
+ when the tag instance is created). If the command was
+ successful, the currently selected sector number is updated
+ and further :meth:`read` and :meth:`write` commands will be
+ relative to that sector.
+
+ Command execution errors raise :exc:`Type2TagCommandError`.
+
+ """
+ if sector != self._current_sector:
+ log.debug("select sector {0} (pages {1} to {2})".format(
+ sector, sector << 10, ((sector+1) << 8) - 1))
+
+ sector_select_1 = b'\xC2\xFF'
+ sector_select_2 = pack('Bxxx', sector)
+
+ rsp = self.transceive(sector_select_1)
+ if len(rsp) == 1 and rsp[0] == 0x0A:
+ try:
+ # command is passively ack'd, i.e. there's no response
+ # and we must make sure there's no retries attempted
+ self.transceive(sector_select_2, timeout=0.001, retries=0)
+ except Type2TagCommandError as error:
+ assert int(error) == TIMEOUT_ERROR # passive ack
+ else:
+ log.debug("sector {0} does not exist".format(sector))
+ raise Type2TagCommandError(INVALID_SECTOR_ERROR)
+ else:
+ log.debug("sector select is not supported for this tag")
+ raise Type2TagCommandError(INVALID_SECTOR_ERROR)
+
+ log.debug("sector {0} is now selected".format(sector))
+ self._current_sector = sector
+ return self._current_sector
+
+ def transceive(self, data, timeout=0.1, retries=2):
+ """Send a Type 2 Tag command and receive the response.
+
+ :meth:`transceive` is a type 2 tag specific wrapper around the
+ :meth:`nfc.ContactlessFrontend.exchange` method. It can be
+ used to send custom commands as a sequence of *data* bytes to
+ the tag and receive the response data bytes. If *timeout*
+ seconds pass without a response, the operation is aborted and
+ :exc:`~nfc.tag.TagCommandError` raised with the TIMEOUT_ERROR
+ error code.
+
+ Command execution errors raise :exc:`Type2TagCommandError`.
+
+ """
+ log.debug(">> {0} ({1:f}s)".format(hexlify(data).decode(), timeout))
+
+ if not self.target:
+ # Sometimes we have to (re)sense the target during
+ # communication. If that failed (tag gone) then any
+ # further attempt to transceive() is the same as
+ # "unrecoverable timeout error".
+ raise Type2TagCommandError(src.lib.nfc.tag.TIMEOUT_ERROR)
+
+ started = time.time()
+ error = None
+ for retry in range(1 + retries):
+ try:
+ data = self.clf.exchange(data, timeout)
+ break
+ except src.lib.nfc.clf.CommunicationError as e:
+ error = e
+ reason = error.__class__.__name__
+ log.debug("%s after %d retries" % (reason, retry))
+ else:
+ if type(error) is src.lib.nfc.clf.TimeoutError:
+ raise Type2TagCommandError(src.lib.nfc.tag.TIMEOUT_ERROR)
+ if type(error) is src.lib.nfc.clf.TransmissionError:
+ raise Type2TagCommandError(src.lib.nfc.tag.RECEIVE_ERROR)
+ if type(error) is src.lib.nfc.clf.ProtocolError:
+ raise Type2TagCommandError(src.lib.nfc.tag.PROTOCOL_ERROR)
+ raise RuntimeError("unexpected " + repr(error))
+
+ elapsed = time.time() - started
+ log.debug("<< {0} ({1:f}s)".format(hexlify(data).decode(), elapsed))
+ return data
+
+
+class Type2TagMemoryReader(object):
+ """The memory reader provides a convenient way to read and write
+ :class:`Type2Tag` memory. Once instantiated with a proper type
+ 2 *tag* object the tag memory can then be accessed as a linear
+ sequence of bytes, without any considerations of sector or
+ page boundaries. Modified bytes can be written to tag memory
+ with :meth:`synchronize`. ::
+
+ clf = nfc.ContactlessFrontend(...)
+ tag = clf.connect(rdwr={'on-connect': None})
+ if isinstance(tag, nfc.tag.tt2.Type2Tag):
+ tag_memory = nfc.tag.tt2.Type2TagMemoryReader(tag)
+ tag_memory[16:19] = [0x03, 0x00, 0xFE]
+ tag_memory.synchronize()
+
+ """
+ def __init__(self, tag):
+ assert isinstance(tag, Type2Tag)
+ self._data_from_tag = bytearray()
+ self._data_in_cache = bytearray()
+ self._tag = tag
+
+ def __len__(self):
+ return len(self._data_from_tag)
+
+ def __getitem__(self, key):
+ if isinstance(key, slice):
+ start, stop, step = key.indices(0x100000)
+ if stop > len(self):
+ self._read_from_tag(stop)
+ elif key >= len(self):
+ self._read_from_tag(stop=key+1)
+ return self._data_in_cache[key]
+
+ def __setitem__(self, key, value):
+ self.__getitem__(key)
+ if isinstance(key, slice):
+ if len(value) != len(range(*key.indices(0x100000))):
+ msg = "{cls} requires item assignment of identical length"
+ raise ValueError(msg.format(cls=self.__class__.__name__))
+ self._data_in_cache[key] = value
+ del self._data_in_cache[len(self):]
+
+ def __delitem__(self, key):
+ msg = "{cls} object does not support item deletion"
+ raise TypeError(msg.format(cls=self.__class__.__name__))
+
+ def _read_from_tag(self, stop):
+ index = (len(self) >> 4) << 4
+ while index < stop:
+ self._tag.sector_select(index >> 10)
+ data = self._tag.read(index >> 2)
+ self._data_from_tag[index:] = data
+ self._data_in_cache[index:] = data
+ index += 16
+
+ def _write_to_tag(self, stop):
+ index = 0
+ while index < stop:
+ data = self._data_in_cache[index:index+4]
+ if data != self._data_from_tag[index:index+4]:
+ self._tag.sector_select(index >> 10)
+ self._tag.write(index >> 2, data)
+ self._data_from_tag[index:index+4] = data
+ index += 4
+
+ def synchronize(self):
+ """Write pages that contain modified data back to tag memory."""
+ self._write_to_tag(stop=len(self))
+
+
+def activate(clf, target):
+ # Type 2 Tags go mute when they receive an unsupported command. It
+ # is then necessary to sense again and by copying sdd_res to
+ # sel_req we ensure that only the same tag will be found.
+ target.sel_req = target.sdd_res[:]
+ if target.sdd_res[0] == 0x04: # NXP
+ import src.lib.nfc.tag.tt2_nxp
+ tag = src.lib.nfc.tag.tt2_nxp.activate(clf, target)
+ if tag is not None:
+ return tag
+ else:
+ # make sure the tag is still alive
+ target = clf.sense(target)
+ if target:
+ return Type2Tag(clf, target)
diff --git a/src/lib/nfc/tag/tt2_nxp.py b/src/lib/nfc/tag/tt2_nxp.py
new file mode 100644
index 0000000..623a5fc
--- /dev/null
+++ b/src/lib/nfc/tag/tt2_nxp.py
@@ -0,0 +1,771 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2014, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import src.lib.nfc.clf
+from . import tt2
+
+import os
+import struct
+from binascii import hexlify
+from pyDes import triple_des, CBC
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class MifareUltralight(tt2.Type2Tag):
+ """Mifare Ultralight is a simple type 2 tag with no specific
+ features. It can store up to 46 byte NDEF message data. This class
+ does not do much more than to provide the known memory size.
+
+ """
+ def __init__(self, clf, target):
+ super(MifareUltralight, self).__init__(clf, target)
+ self._product = "Mifare Ultralight (MF01CU1)"
+
+ def dump(self):
+ return super(MifareUltralight, self)._dump(stop=16)
+
+
+class MifareUltralightC(tt2.Type2Tag):
+ """Mifare Ultralight C provides more memory, to store up to 142 byte
+ NDEF message data, and can be password protected.
+
+ """
+ class NDEF(tt2.Type2Tag.NDEF):
+ def _read_capability_data(self, tag_memory):
+ base_class = super(MifareUltralightC.NDEF, self)
+ if base_class._read_capability_data(tag_memory):
+ if self.tag.is_authenticated:
+ if not self._readable and tag_memory[15] >> 4 == 8:
+ self._readable = True
+ if not self._writeable and tag_memory[15] & 0xF == 8:
+ self._writeable = bool(tag_memory[10:12] == b"\0\0")
+ return True
+ return False
+
+ def __init__(self, clf, target):
+ super(MifareUltralightC, self).__init__(clf, target)
+ self._product = "Mifare Ultralight C (MF01CU2)"
+
+ def dump(self):
+ lines = super(MifareUltralightC, self)._dump(stop=40)
+
+ footer = dict(zip(range(40, 44), (
+ "LOCK2-LOCK3", "CTR0-CTR1", "AUTH0", "AUTH1")))
+
+ for i in sorted(footer.keys()):
+ try:
+ data = self.read(i)[0:4]
+ except tt2.Type2TagCommandError:
+ data = [None, None, None, None]
+ lines.append(tt2.pagedump(i, data, footer[i]))
+
+ return lines
+
+ def protect(self, password=None, read_protect=False, protect_from=0):
+ """Protect a Mifare Ultralight C Tag.
+
+ A Mifare Ultrlight C Tag can be provisioned with a custom
+ password (or the default manufacturer key if the password is
+ an empty string or bytearray).
+
+ A non-empty *password* must provide at least 128 bit key
+ material, in other words it must be a string or bytearray of
+ length 16 or more.
+
+ If *password* is not None, the first protected memory page can
+ be specified with the *protect_from* integer argument. A
+ memory page is 4 byte and the total number of pages is 48. A
+ *protect_from* argument of 48 effectively disables memory
+ protection. A *protect_from* argument of 3 protects all user
+ data pages including the bitwise one-time-programmable page
+ 3. Any value less than 3 or more than 48 is accepted but to
+ the same effect as if 3 or 48 were specified. If effective
+ protection starts at page 3 and the tag is formatted for NDEF,
+ the :meth:`protect` method does also modify the NDEF
+ read/write capability byte.
+
+ If *password* is not None and *read_protect* is True then the
+ tag memory content will also be protected against read access,
+ i.e. successful authentication will be required to read
+ protected pages.
+
+ The :meth:`protect` method verifies a password change by
+ authenticating with the new *password* after all modifications
+ were made and returns the result of :meth:`authenticate`.
+
+ .. warning:: If protect is called without a password, the
+ default Type 2 Tag protection method will set the lock
+ bits to readonly. This process is not reversible.
+
+ """
+ args = (password, read_protect, protect_from)
+ return super(MifareUltralightC, self).protect(*args)
+
+ def _protect(self, password, read_protect, protect_from):
+ if password is None:
+ return self._protect_with_lockbits()
+ else:
+ args = (password, read_protect, protect_from)
+ return self._protect_with_password(*args)
+
+ def _protect_with_lockbits(self):
+ try:
+ ndef_cc = self.read(3)[0:4]
+ if ndef_cc[0] == 0xE1 and ndef_cc[1] >> 4 == 1:
+ ndef_cc[3] = 0x0F
+ self.write(3, ndef_cc)
+ self.write(2, b"\x00\x00\xFF\xFF")
+ self.write(40, b"\xFF\xFF\x00\x00")
+ return True
+ except tt2.Type2TagCommandError:
+ return False
+
+ def _protect_with_password(self, password, read_protect, protect_from):
+ if password and len(password) < 16:
+ raise ValueError("password must be at least 16 byte")
+
+ # The first 16 password character bytes are taken as key
+ # unless the password is empty. If it's empty we use the
+ # factory default password.
+ key = password[0:16] if password != b"" else b"IEMKAERB!NACUOYF"
+ log.debug("protect with key %s", hexlify(key).decode())
+
+ # split the key and reverse
+ key1, key2 = key[7::-1], key[15:7:-1]
+ self.write(44, key1[0:4])
+ self.write(45, key1[4:8])
+ self.write(46, key2[0:4])
+ self.write(47, key2[4:8])
+
+ # protect from memory page
+ self.write(42, bytearray([max(3, min(protect_from, 0x30))]) +
+ b"\0\0\0")
+
+ # set read protection flag
+ self.write(43, b"\0\0\0\0" if read_protect else b"\x01\0\0\0")
+
+ # Set NDEF read/write permissions if protection starts at page
+ # 3 and the tag is formatted for NDEF. We set the read/write
+ # permission flags to 8, thus indicating proprietary access.
+ if protect_from <= 3:
+ ndef_cc = self.read(3)[0:4]
+ if ndef_cc[0] == 0xE1 and ndef_cc[1] & 0xF0 == 0x10:
+ ndef_cc[3] |= (0x88 if read_protect else 0x08)
+ self.write(3, ndef_cc)
+
+ # Reactivate the tag to have the key effective and
+ # authenticate with the same key
+ self._target = self.clf.sense(self.target)
+ return self.authenticate(key) if self.target else False
+
+ def authenticate(self, password):
+ """Authenticate with a Mifare Ultralight C Tag.
+
+ :meth:`autenticate` executes the Mifare Ultralight C mutual
+ authentication protocol to verify that the *password* argument
+ matches the key that is stored in the card. A new card key can
+ be set with :meth:`protect`.
+
+ The *password* argument must be a string with either 0 or at
+ least 16 bytes. A zero length password string indicates that
+ the factory default card key be used. From a password with 16
+ or more bytes the first 16 byte are taken as card key,
+ remaining bytes are ignored. A password length between 1 and
+ 15 generates a ValueError exception.
+
+ The authentication result is True if the password was
+ confirmed and False if not.
+
+ """
+ return super(MifareUltralightC, self).authenticate(password)
+
+ def _authenticate(self, password):
+ # The first 16 password character bytes are taken as key
+ # unless the password is empty. If it's empty we use the
+ # factory default password.
+ key = password[0:16] if password != b"" else b"IEMKAERB!NACUOYF"
+
+ if len(key) != 16:
+ raise ValueError("password must be at least 16 byte")
+
+ log.debug("authenticate with key %s", hexlify(key).decode())
+
+ rsp = self.transceive(b"\x1A\x00")
+ m1 = bytes(rsp[1:9])
+ iv = b"\x00\x00\x00\x00\x00\x00\x00\x00"
+ rb = triple_des(key, CBC, iv).decrypt(m1)
+
+ log.debug("received challenge")
+ log.debug("iv = %s", hexlify(iv).decode())
+ log.debug("m1 = %s", hexlify(m1).decode())
+ log.debug("rb = %s", hexlify(rb).decode())
+
+ ra = os.urandom(8)
+ iv = bytes(rsp[1:9])
+
+ m2 = triple_des(key, CBC, iv).encrypt(ra + rb[1:8] + (
+ struct.pack("B", rb[0]) if isinstance(rb[0], int) else rb[0]))
+
+ log.debug("sending response")
+ log.debug("ra = %s", hexlify(ra).decode())
+ log.debug("iv = %s", hexlify(iv).decode())
+ log.debug("m2 = %s", hexlify(m2).decode())
+ try:
+ rsp = self.transceive(b"\xAF" + m2)
+ except tt2.Type2TagCommandError:
+ return False
+
+ m3 = bytes(rsp[1:9])
+ iv = m2[8:16]
+ log.debug("received confirmation")
+ log.debug("iv = %s", hexlify(iv).decode())
+ log.debug("m3 = %s", hexlify(m3).decode())
+
+ return triple_des(key, CBC, iv).decrypt(m3) == ra[1:9] \
+ + (struct.pack("B", ra[0]) if isinstance(ra[0], int) else ra[0])
+
+
+class NTAG203(tt2.Type2Tag):
+ """The NTAG203 is a plain memory Tag with 144 bytes user data memory
+ plus a 16-bit one-way counter. It does not have any security
+ features beyond the standard lock bit mechanism that permanently
+ disables write access.
+
+ """
+ def __init__(self, clf, target):
+ super(NTAG203, self).__init__(clf, target)
+ self._product = "NXP NTAG203"
+
+ def dump(self):
+ lines = super(NTAG203, self)._dump(40)
+
+ footer = dict(zip(range(40, 42), ("LOCK2-LOCK3", "CNTR0-CNTR1")))
+
+ for i in sorted(footer.keys()):
+ try:
+ data = self.read(i)[0:4]
+ except tt2.Type2TagCommandError:
+ data = [None, None, None, None]
+ lines.append(tt2.pagedump(i, data, footer[i]))
+
+ return lines
+
+ def protect(self, password=None, read_protect=False, protect_from=0):
+ """Set lock bits to disable future memory modifications.
+
+ If *password* is None, all memory pages except the 16-bit
+ counter in page 41 are protected by setting the relevant lock
+ bits (note that lock bits can not be reset). If valid NDEF
+ management data is found in page 4, protect() also sets the
+ NDEF write flag to read-only.
+
+ The NTAG203 can not be password protected. If a *password*
+ argument is provided, the protect() method always returns
+ False.
+
+ """
+ return super(NTAG203, self).protect(
+ password, read_protect, protect_from)
+
+ def _protect(self, password, read_protect, protect_from):
+ if password is None:
+ try:
+ ndef_cc = self.read(3)[0:4]
+ if ndef_cc[0] == 0xE1 and ndef_cc[1] >> 4 == 1:
+ ndef_cc[3] = 0x0F
+ self.write(3, ndef_cc)
+ self.write(2, b"\x00\x00\xFF\xFF")
+ self.write(40, b"\xFF\x01\x00\x00")
+ return True
+ except tt2.Type2TagCommandError:
+ pass
+ return False
+
+ def _format(self, version, wipe):
+ if self.ndef is None:
+ log.debug("no management data, writing factory defaults")
+ self.write(4, b'\x01\x03\xA0\x10')
+ self.write(5, b'\x44\x03\x00\xFE')
+ return super(NTAG203, self)._format(version, wipe)
+
+
+class NTAG21x(tt2.Type2Tag):
+ """Base class for the NTAG21x family (210/212/213/215/216). The
+ methods and attributes documented here are supported for all
+ NTAG21x products.
+
+ All NTAG21x products support a simple password protection scheme
+ that can be configured to restrict write as well as read access to
+ memory starting from a selected page address. A factory programmed
+ ECC signature allows to verify the tag unique identifier.
+
+ """
+ class NDEF(tt2.Type2Tag.NDEF):
+ def _read_capability_data(self, tag_memory):
+ if super(NTAG21x.NDEF, self)._read_capability_data(tag_memory):
+ if self.tag.is_authenticated:
+ if not self._readable and tag_memory[15] >> 4 == 8:
+ self._readable = True
+ if not self._writeable and tag_memory[15] & 0xF == 8:
+ self._writeable = bool(tag_memory[10:12] == b"\0\0")
+ return True
+ return False
+
+ @property
+ def signature(self):
+ """The 32-byte ECC tag signature programmed at chip production. The
+ signature is provided as a string and can only be read.
+
+ The signature attribute is always loaded from the tag when it
+ is accessed, i.e. it is not cached. If communication with the
+ tag fails for some reason the signature attribute is set to a
+ 32-byte string of all zeros.
+
+ """
+ log.debug("read tag signature")
+ try:
+ return bytes(self.transceive(b"\x3C\x00"))
+ except tt2.Type2TagCommandError:
+ return 32 * b"\0"
+
+ def protect(self, password=None, read_protect=False, protect_from=0):
+ """Set password protection or permanent lock bits.
+
+ If the *password* argument is None, all memory pages will be
+ protected by setting the relevant lock bits (note that lock
+ bits can not be reset). If valid NDEF management data is
+ found, protect() also sets the NDEF write flag to read-only.
+
+ All Tags of the NTAG21x family can alternatively be protected
+ by password. If a *password* argument is provided, the
+ protect() method writes the first 4 byte of the *password*
+ string into the Tag's password (PWD) memory bytes and the
+ following 2 byte of the *password* string into the password
+ acknowledge (PACK) memory bytes. Factory default values are
+ used if the *password* argument is an empty string. Lock bits
+ are not set for password protection.
+
+ The *read_protect* and *protect_from* arguments are only
+ evaluated if *password* is not None. If *read_protect* is
+ True, the memory protection bit (PROT) is set to require
+ password verification also for reading of protected memory
+ pages. The value of *protect_from* determines the first
+ password protected memory page (one page is 4 byte) with the
+ exception that the smallest set value is page 3 even if
+ *protect_from* is smaller.
+
+ """
+ args = (password, read_protect, protect_from)
+ return super(NTAG21x, self).protect(*args)
+
+ def _protect(self, password, read_protect, protect_from):
+ if password is None:
+ return self._protect_with_lockbits()
+ else:
+ args = (password, read_protect, protect_from)
+ return self._protect_with_password(*args)
+
+ def _protect_with_lockbits(self):
+ try:
+ ndef_cc = self.read(3)[0:4]
+ if ndef_cc[0] == 0xE1 and ndef_cc[1] >> 4 == 1:
+ ndef_cc[3] = 0x0F
+ self.write(3, ndef_cc)
+ self.write(2, b"\x00\x00\xFF\xFF")
+ if self._cfgpage > 16:
+ self.write(self._cfgpage - 1, b"\xFF\xFF\xFF\x00")
+ cfgdata = self.read(self._cfgpage)
+ if cfgdata[4] & 0x40 == 0:
+ cfgdata[4] |= 0x40 # set CFGLCK bit
+ self.write(self._cfgpage + 1, cfgdata[4:8])
+ return True
+ except tt2.Type2TagCommandError:
+ return False
+
+ def _protect_with_password(self, password, read_protect, protect_from):
+ if password and len(password) < 6:
+ raise ValueError("password must be at least 6 bytes")
+
+ key = password[0:6] if password != b"" else b"\xFF\xFF\xFF\xFF\0\0"
+ log.debug("protect with key %s", hexlify(key).decode())
+
+ # read CFG0, CFG1, PWD and PACK
+ cfg = self.read(self._cfgpage)
+
+ # set password and acknowledge
+ cfg[8:14] = key
+
+ # start protection from page
+ cfg[3] = max(3, min(protect_from, 255))
+
+ # set read protection bit
+ cfg[4] = cfg[4] | 0x80 if read_protect else cfg[4] & 0x7F
+
+ # write configuration to tag
+ for i in range(4):
+ self.write(self._cfgpage + i, cfg[i*4:(i+1)*4])
+
+ # Set NDEF read/write permissions if protection starts at page
+ # 3 and the tag is formatted for NDEF. We set the read/write
+ # permission flags to 8, thus indicating proprietary access.
+ if protect_from <= 3:
+ ndef_cc = self.read(3)[0:4]
+ if ndef_cc[0] == 0xE1 and ndef_cc[1] & 0xF0 == 0x10:
+ ndef_cc[3] |= (0x88 if read_protect else 0x08)
+ self.write(3, ndef_cc)
+
+ # Reactivate the tag to have the key effective and
+ # authenticate with the same key
+ self._target = self.clf.sense(self.target)
+ return self.authenticate(key) if self.target else False
+
+ def authenticate(self, password):
+ """Authenticate with password to access protected memory.
+
+ An NTAG21x implements a simple password protection scheme. The
+ reader proofs possession of a share secret by sending a 4-byte
+ password and the tag proofs possession of a shared secret by
+ returning a 2-byte password acknowledge. Because password and
+ password acknowledge are transmitted in plain text special
+ considerations should be given to under which conditions
+ authentication is performed. If, for example, an attacker is
+ able to mount a relay attack both secret values are easily
+ lost.
+
+ The *password* argument must be a string of length zero or at
+ least 6 byte characters. If the *password* length is zero,
+ authentication is performed with factory default values. If
+ the *password* contains at least 6 bytes, the first 4 byte are
+ send to the tag as the password secret and the following 2
+ byte are compared against the password acknowledge that is
+ received from the tag.
+
+ The authentication result is True if the password was
+ confirmed and False if not.
+
+ """
+ return super(NTAG21x, self).authenticate(password)
+
+ def _authenticate(self, password):
+ if password and len(password) < 6:
+ raise ValueError("password must be at least 6 bytes")
+
+ key = password[0:6] if password != b"" else b"\xFF\xFF\xFF\xFF\0\0"
+ log.debug("authenticate with key %s", hexlify(key).decode())
+
+ try:
+ rsp = self.transceive(b"\x1B" + key[0:4])
+ return rsp == key[4:6]
+ except tt2.Type2TagCommandError:
+ return False
+
+ def _dump(self, stop, footer):
+ lines = super(NTAG21x, self)._dump(stop)
+ for i in sorted(footer.keys()):
+ try:
+ data = self.read(i)[0:4]
+ except tt2.Type2TagCommandError:
+ data = [None, None, None, None]
+ lines.append(tt2.pagedump(i, data, footer[i]))
+ return lines
+
+
+class NTAG210(NTAG21x):
+ """The NTAG210 provides 48 bytes user data memory, password
+ protection, originality signature and a UID mirror function.
+
+ """
+ def __init__(self, clf, target):
+ super(NTAG210, self).__init__(clf, target)
+ self._product = "NXP NTAG210"
+ self._cfgpage = 16
+
+ def _format(self, version, wipe):
+ if self.ndef is None:
+ log.debug("no management data, writing factory defaults")
+ self.write(4, b'\x03\x00\xFE\x00')
+ self.write(5, b'\x00\x00\x00\x00')
+ return super(NTAG210, self)._format(version, wipe)
+
+ def dump(self):
+ footer = dict(zip(range(16, 20),
+ ("MIRROR_BYTE, RFU, MIRROR_PAGE, AUTH0",
+ "ACCESS", "PWD0-PWD3", "PACK0-PACK1")))
+ return super(NTAG210, self)._dump(16, footer)
+
+
+class NTAG212(NTAG21x):
+ """The NTAG212 provides 128 bytes user data memory, password
+ protection, originality signature and a UID mirror function.
+
+ """
+ def __init__(self, clf, target):
+ super(NTAG212, self).__init__(clf, target)
+ self._product = "NXP NTAG212"
+ self._cfgpage = 37
+
+ def _format(self, version, wipe):
+ if self.ndef is None:
+ log.debug("no management data, writing factory defaults")
+ self.write(4, b'\x01\x03\x90\x0A')
+ self.write(5, b'\x34\x03\x00\xFE')
+ return super(NTAG212, self)._format(version, wipe)
+
+ def dump(self):
+ text = ("LOCK2-LOCK4", "MIRROR_BYTE, RFU, MIRROR_PAGE, AUTH0",
+ "ACCESS", "PWD0-PWD3", "PACK0-PACK1")
+ footer = dict(zip(range(36, 36+len(text)), text))
+ return super(NTAG212, self)._dump(36, footer)
+
+
+class NTAG213(NTAG21x):
+ """The NTAG213 provides 144 bytes user data memory, password
+ protection, originality signature, a tag read counter and a mirror
+ function for the tag unique identifier and the read counter.
+
+ """
+ def __init__(self, clf, target):
+ super(NTAG213, self).__init__(clf, target)
+ self._product = "NXP NTAG213"
+ self._cfgpage = 41
+
+ def _format(self, version, wipe):
+ if self.ndef is None:
+ log.debug("no management data, writing factory defaults")
+ self.write(4, b'\x01\x03\xA0\x0C')
+ self.write(5, b'\x34\x03\x00\xFE')
+ return super(NTAG213, self)._format(version, wipe)
+
+ def dump(self):
+ text = ("LOCK2-LOCK4", "MIRROR, RFU, MIRROR_PAGE, AUTH0",
+ "ACCESS", "PWD0-PWD3", "PACK0-PACK1")
+ footer = dict(zip(range(40, 40+len(text)), text))
+ return super(NTAG213, self)._dump(40, footer)
+
+
+class NTAG215(NTAG21x):
+ """The NTAG215 provides 504 bytes user data memory, password
+ protection, originality signature, a tag read counter and a mirror
+ function for the tag unique identifier and the read counter.
+
+ """
+ def __init__(self, clf, target):
+ super(NTAG215, self).__init__(clf, target)
+ self._product = "NXP NTAG215"
+ self._cfgpage = 131
+
+ def _format(self, version, wipe):
+ if self.ndef is None:
+ log.debug("no management data, writing factory defaults")
+ self.write(4, b'\x03\x00\xFE\x00')
+ self.write(5, b'\x00\x00\x00\x00')
+ return super(NTAG215, self)._format(version, wipe)
+
+ def dump(self):
+ text = ("LOCK2-LOCK4", "MIRROR, RFU, MIRROR_PAGE, AUTH0",
+ "ACCESS", "PWD0-PWD3", "PACK0-PACK1")
+ footer = dict(zip(range(130, 130+len(text)), text))
+ return super(NTAG215, self)._dump(130, footer)
+
+
+class NTAG216(NTAG21x):
+ """The NTAG216 provides 888 bytes user data memory, password
+ protection, originality signature, a tag read counter and a mirror
+ function for the tag unique identifier and the read counter.
+
+ """
+ def __init__(self, clf, target):
+ super(NTAG216, self).__init__(clf, target)
+ self._product = "NXP NTAG216"
+ self._cfgpage = 227
+
+ def _format(self, version, wipe):
+ if self.ndef is None:
+ log.debug("no management data, writing factory defaults")
+ self.write(4, b'\x03\x00\xFE\x00')
+ self.write(5, b'\x00\x00\x00\x00')
+ return super(NTAG216, self)._format(version, wipe)
+
+ def dump(self):
+ text = ("LOCK2-LOCK4", "MIRROR, RFU, MIRROR_PAGE, AUTH0",
+ "ACCESS", "PWD0-PWD3", "PACK0-PACK1")
+ footer = dict(zip(range(226, 226+len(text)), text))
+ return super(NTAG216, self)._dump(226, footer)
+
+
+class MifareUltralightEV1(NTAG21x):
+ """Mifare Ultralight EV1
+
+ """
+ def __init__(self, clf, target, product):
+ super(MifareUltralightEV1, self).__init__(clf, target)
+ self._product = "Mifare Ultralight EV1 ({0})".format(product)
+
+ def _dump_ul11(self):
+ text = ("MOD, RFU, RFU, AUTH0", "ACCESS, VCTID, RFU, RFU",
+ "PWD0, PWD1, PWD2, PWD3", "PACK0, PACK1, RFU, RFU")
+ footer = dict(zip(range(16, 16+len(text)), text))
+ return super(MifareUltralightEV1, self)._dump(16, footer)
+
+ def _dump_ul21(self):
+ text = ("LOCK2, LOCK3, LOCK4, RFU",
+ "MOD, RFU, RFU, AUTH0", "ACCESS, VCTID, RFU, RFU",
+ "PWD0, PWD1, PWD2, PWD3", "PACK0, PACK1, RFU, RFU")
+ footer = dict(zip(range(36, 36+len(text)), text))
+ return super(MifareUltralightEV1, self)._dump(36, footer)
+
+
+class MF0UL11(MifareUltralightEV1):
+ def __init__(self, clf, target):
+ super(MF0UL11, self).__init__(clf, target, "MF0UL11")
+
+ def dump(self):
+ return self._dump_ul11()
+
+
+class MF0ULH11(MifareUltralightEV1):
+ def __init__(self, clf, target):
+ super(MF0ULH11, self).__init__(clf, target, "MF0ULH11")
+
+ def dump(self):
+ return self._dump_ul11()
+
+
+class MF0UL21(MifareUltralightEV1):
+ def __init__(self, clf, target):
+ super(MF0UL21, self).__init__(clf, target, "MF0UL21")
+
+ def dump(self):
+ return self._dump_ul21()
+
+
+class MF0ULH21(MifareUltralightEV1):
+ def __init__(self, clf, target):
+ super(MF0ULH21, self).__init__(clf, target, "MF0ULH21")
+
+ def dump(self):
+ return self._dump_ul21()
+
+
+class NTAGI2C(tt2.Type2Tag):
+ def _dump(self, stop):
+ s = super(NTAGI2C, self)._dump(stop)
+
+ data = self.read(stop)[0:4]
+ s.append(tt2.pagedump(stop, data, "LOCK2-LOCK4, CHK"))
+
+ data = self.read(232)
+ s.append("")
+ s.append("Configuration registers:")
+ s.append(tt2.pagedump(stop & 256 | 232, data[0:4],
+ "NC, LD, SM, WDT0"))
+ s.append(tt2.pagedump(stop & 256 | 233, data[4:8],
+ "WDT1, CLK, LOCK, RFU"))
+
+ self.sector_select(3)
+ data = self.read(248)
+ s.append("")
+ s.append("Session registers:")
+ s.append(tt2.pagedump(0x3F8, data[0:4], "NC, LD, SM, WDT0"))
+ s.append(tt2.pagedump(0x3F9, data[4:8], "WDT1, CLK, NS, RFU"))
+
+ self.sector_select(0)
+ return s
+
+
+class NT3H1101(NTAGI2C):
+ """NTAG I2C 1K.
+
+ """
+ def __init__(self, clf, target):
+ super(NT3H1101, self).__init__(clf, target)
+ self._product = "NTAG I2C 1K (NT3H1101)"
+
+ def dump(self):
+ return super(NT3H1101, self)._dump(226)
+
+
+class NT3H1201(NTAGI2C):
+ """NTAG I2C 2K.
+
+ """
+ def __init__(self, clf, target):
+ super(NT3H1201, self).__init__(clf, target)
+ self._product = "NTAG I2C 2K (NT3H1201)"
+
+ def dump(self):
+ return super(NT3H1201, self)._dump(480)
+
+
+VERSION_MAP = {
+ b"\x00\x04\x03\x01\x01\x00\x0B\x03": MF0UL11,
+ b"\x00\x04\x03\x02\x01\x00\x0B\x03": MF0ULH11,
+ b"\x00\x04\x03\x01\x01\x00\x0E\x03": MF0UL21,
+ b"\x00\x04\x03\x02\x01\x00\x0E\x03": MF0ULH21,
+ b"\x00\x04\x04\x01\x01\x00\x0B\x03": NTAG210,
+ b"\x00\x04\x04\x01\x01\x00\x0E\x03": NTAG212,
+ b"\x00\x04\x04\x02\x01\x00\x0F\x03": NTAG213,
+ b"\x00\x04\x04\x02\x01\x00\x11\x03": NTAG215,
+ b"\x00\x04\x04\x02\x01\x00\x13\x03": NTAG216,
+ b"\x00\x04\x04\x05\x02\x01\x13\x03": NT3H1101,
+ b"\x00\x04\x04\x05\x02\x01\x15\x03": NT3H1201,
+ # b"\x00\x04\x04\x05\x02\x02\x13\x03": NT3H2111,
+ # b"\x00\x04\x04\x05\x02\x02\x15\x03": NT3H2211,
+}
+
+
+def activate(clf, target):
+ log.debug("check if authenticate command is available")
+ try:
+ rsp = clf.exchange(b'\x1A\x00', timeout=0.01)
+ if clf.sense(target) is None:
+ return
+ if rsp.startswith(b"\xAF"):
+ return MifareUltralightC(clf, target)
+ except src.lib.nfc.clf.TimeoutError:
+ if clf.sense(target) is None:
+ return
+ except src.lib.nfc.clf.CommunicationError as error:
+ log.debug(repr(error))
+ return
+
+ log.debug("check if version command is available")
+ try:
+ rsp = bytes(clf.exchange(b'\x60', timeout=0.01))
+ if rsp in VERSION_MAP:
+ return VERSION_MAP[rsp](clf, target)
+ if rsp == b"\x00":
+ if clf.sense(target) is None:
+ return None
+ else:
+ return NTAG203(clf, target)
+ log.debug("no match for version %s", hexlify(rsp).decode().upper())
+ return
+ except src.lib.nfc.clf.TimeoutError:
+ if clf.sense(target) is None:
+ return
+ except src.lib.nfc.clf.CommunicationError as error:
+ log.debug(repr(error))
+ return
+
+ return MifareUltralight(clf, target)
diff --git a/src/lib/nfc/tag/tt3.py b/src/lib/nfc/tag/tt3.py
new file mode 100644
index 0000000..d77a5f9
--- /dev/null
+++ b/src/lib/nfc/tag/tt3.py
@@ -0,0 +1,930 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2009, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import nfc.tag
+import nfc.clf
+
+import math
+import time
+import itertools
+from binascii import hexlify
+from struct import pack, unpack
+
+import logging
+log = logging.getLogger(__name__)
+
+
+RSP_LENGTH_ERROR, RSP_CODE_ERROR, TAG_IDM_ERROR, DATA_SIZE_ERROR = range(1, 5)
+
+
+class Type3TagCommandError(nfc.tag.TagCommandError):
+ errno_str = {
+ RSP_LENGTH_ERROR: "invalid response length",
+ RSP_CODE_ERROR: "invalid response code",
+ TAG_IDM_ERROR: "answer from wrong tag",
+ DATA_SIZE_ERROR: "insufficient data received",
+ # FeliCa Lite specific error codes
+ 0x01A6: "invalid service code number or attribute",
+ 0x01B1: "authentication required to read (first block in list)",
+ 0x02B1: "authentication required to read (second block in list)",
+ 0x04B1: "authentication required to read (third block in list)",
+ 0x08B1: "authentication required to read (fourth block in list)",
+ 0x02B2: "verification failure for write with mac operation",
+ }
+
+
+class ServiceCode:
+ """A service code provides access to a group of data blocks located on
+ the card file system. A service code is a 16-bit structure
+ composed of a 10-bit service number and a 6-bit service
+ attribute. The service attribute determines the service type and
+ whether authentication is required.
+
+ """
+ def __init__(self, number, attribute):
+ self.number = number
+ self.attribute = attribute
+
+ def __repr__(self):
+ return "ServiceCode({0}, {1})".format(self.number, self.attribute)
+
+ def __str__(self):
+ attribute_map = {
+ 0b001000: "Random RW with key",
+ 0b001001: "Random RW w/o key",
+ 0b001010: "Random RO with key",
+ 0b001011: "Random RO w/o key",
+ 0b001100: "Cyclic RW with key",
+ 0b001101: "Cyclic RW w/o key",
+ 0b001110: "Cyclic RO with key",
+ 0b001111: "Cyclic RO w/o key",
+ 0b010000: "Purse Direct with key",
+ 0b010001: "Purse Direct w/o key",
+ 0b010010: "Purse Cashback with key",
+ 0b010011: "Purse Cashback w/o key",
+ 0b010100: "Purse Decrement with key",
+ 0b010101: "Purse Decrement w/o key",
+ 0b010110: "Purse Read Only with key",
+ 0b010111: "Purse Read Only w/o key",
+ }
+ try:
+ attribute_string = attribute_map[self.attribute]
+ except KeyError:
+ attribute_string = "Type {0:06b}b".format(self.attribute)
+ return "Service Code {0:04X}h (Service {1} {2!s})".format(
+ int(self), self.number, attribute_string)
+
+ def __int__(self):
+ return self.number << 6 | self.attribute
+
+ def pack(self):
+ """Pack the service code for transmission. Returns a 2 byte string."""
+ sn, sa = self.number, self.attribute
+ return pack("> 6, v & 0x3f)
+
+
+class BlockCode:
+ """A block code indicates a data block within a service. A block code
+ is a 16-bit or 24-bit structure composed of a length bit (1b if
+ the block number is less than 256), a 3-bit access mode, a 4-bit
+ service list index and an 8-bit or 16-bit block number.
+
+ """
+ def __init__(self, number, access=0, service=0):
+ self.number = number
+ self.access = access
+ self.service = service
+
+ def __repr__(self):
+ return "BlockCode({0}, {1}, {2})".format(
+ self.number, self.access, self.service)
+
+ def __str__(self):
+ s = "BlockCode(number={0}, access={1:03b}, service={2})"
+ return s.format(self.number, self.access, self.service)
+
+ def __bytes__(self):
+ return str(self).encode()
+
+ def pack(self):
+ """Pack the block code for transmission. Returns a 2-3 byte string."""
+ bn, am, sx = self.number, self.access, self.service
+ return bytes(
+ bytearray([bool(bn < 256) << 7 | (am & 0x7) << 4 | (sx & 0xf)])
+ + (bytearray([bn]) if bn < 256 else pack("H", data[14:16])[0]:
+ log.debug("ndef attribute data checksum error")
+ return None
+
+ ver, nbr, nbw, nmaxb = unpack(">BBBH", data[0:5])
+ writef, rwflag = unpack(">BB", data[9:11])
+ length = unpack(">I", b"\x00" + data[11:14])[0]
+ self._capacity = nmaxb * 16
+ self._writeable = rwflag != 0 and nbw > 0
+ self._readable = writef == 0 and nbr > 0
+ attributes = {
+ 'ver': ver, 'nbr': nbr, 'nbw': nbw, 'nmaxb': nmaxb,
+ 'writef': writef, 'rwflag': rwflag, 'ln': length}
+ log.debug("got ndef attributes {0}".format(attributes))
+ return attributes
+
+ def _write_attribute_data(self, attributes):
+ log.debug("set ndef attributes {0}".format(attributes))
+ attribute_data = bytearray(16)
+ attribute_data[0] = attributes['ver']
+ attribute_data[1] = attributes['nbr']
+ attribute_data[2] = attributes['nbw']
+ attribute_data[3:5] = pack('>H', attributes['nmaxb'])
+ attribute_data[9] = attributes['writef']
+ attribute_data[10] = attributes['rwflag']
+ attribute_data[11:14] = pack('>I', attributes['ln'])[1:4]
+ attribute_data[14:16] = pack('>H', sum(attribute_data[0:14]))
+ self._tag.write_to_ndef_service(attribute_data, 0)
+
+ def _read_ndef_data(self):
+ if self.tag.sys != 0x12FC:
+ try:
+ self.tag.idm, self.tag.pmm = self._tag.polling(0x12FC)
+ self.tag.sys = 0x12FC
+ except Type3TagCommandError:
+ return None
+
+ attributes = self._read_attribute_data()
+ if attributes is None:
+ log.debug("found no attribute data (maybe checksum error)")
+ return None
+ if attributes['ver'] >> 4 != 1:
+ log.debug("unsupported ndef mapping major version")
+ return None
+
+ last_block_number = 1 + (attributes['ln'] + 15) // 16
+ data = bytearray()
+
+ for i in range(1, last_block_number, attributes['nbr']):
+ last_block = min(i + attributes['nbr'], last_block_number)
+ block_list = range(i, last_block)
+ try:
+ data += self.tag.read_from_ndef_service(*block_list)
+ except Type3TagCommandError:
+ return None
+
+ data = data[0:attributes['ln']]
+ log.debug("got {0} byte ndef data {1}{2}".format(
+ len(data),
+ hexlify(data[0:32]).decode(),
+ ('', '...')[len(data) > 32]))
+
+ return data
+
+ def _write_ndef_data(self, data):
+ attributes = self._read_attribute_data()
+ attributes['writef'] = 0x0F
+ self._write_attribute_data(attributes)
+
+ log.debug("set {0} byte ndef data {1}{2}".format(
+ len(data),
+ hexlify(data[0:32]).decode(),
+ ('', '...')[len(data) > 32]))
+
+ last_block_number = 1 + (len(data) + 15) // 16
+ attributes['ln'] = len(data) # because we may need to pad zeros
+ data = data + bytearray(-len(data) % 16) # adjust to block size
+
+ for i in range(1, last_block_number, attributes['nbw']):
+ last_block = min(i + attributes['nbw'], last_block_number)
+ block_data = data[(i-1)*16:(last_block-1)*16]
+ self._tag.write_to_ndef_service(
+ block_data, *range(i, last_block))
+
+ attributes['writef'] = 0x00
+ self._write_attribute_data(attributes)
+ return True
+
+ def __init__(self, clf, target):
+ super(Type3Tag, self).__init__(clf, target)
+ self.idm = target.sensf_res[1:9]
+ self.pmm = target.sensf_res[9:17]
+ self.sys = 0xFFFF
+ if len(target.sensf_res) > 17:
+ self.sys = unpack(">H", target.sensf_res[17:19])[0]
+ self._nfcid = bytearray(self.idm)
+
+ def __str__(self):
+ s = " PMM={pmm} SYS={sys:04X}"
+ return nfc.tag.Tag.__str__(self) + s.format(
+ pmm=hexlify(self.pmm).decode().upper(), sys=self.sys)
+
+ def _is_present(self):
+ # Check if the card still responds to the acquired system code
+ # and the returned identifier (IDm) matches. This is called
+ # from nfc.tag.Tag for the 'is_present' attribute.
+ try:
+ idm, pmm = self.polling(self.sys)
+ return idm == self.identifier
+ except Type3TagCommandError:
+ return False
+
+ def dump(self):
+ """Read all data blocks of an NFC Forum Tag.
+
+ For an NFC Forum Tag (system code 0x12FC) :meth:`dump` reads
+ all data blocks from service 0x000B (NDEF read service) and
+ returns a list of strings suitable for printing. The number of
+ strings returned does not necessarily reflect the number of
+ data blocks because a range of data blocks with equal content
+ is reduced to fewer lines of output.
+
+ """
+ if self.sys == 0x12FC:
+ ndef_read_service = ServiceCode(0, 0b01011)
+ return self.dump_service(ndef_read_service)
+ else:
+ return ["This is not an NFC Forum Tag."]
+
+ def dump_service(self, sc):
+ """Read all data blocks of a given service.
+
+ :meth:`dump_service` reads all data blocks from the service
+ with service code *sc* and returns a list of strings suitable
+ for printing. The number of strings returned does not
+ necessarily reflect the number of data blocks because a range
+ of data blocks with equal content is reduced to fewer lines of
+ output.
+
+ """
+ def lprint(fmt, data, index):
+ ispchr = lambda x: x >= 32 and x <= 126 # noqa: E731
+
+ def print_bytes(octets):
+ return ' '.join(['%02x' % x for x in octets])
+
+ def print_chars(octets):
+ return ''.join([chr(x) if ispchr(x) else '.' for x in octets])
+
+ return fmt.format(index, print_bytes(data), print_chars(data))
+
+ data_line_fmt = "{0:04X}: {1} |{2}|"
+ same_line_fmt = "{0:<4s} {1} |{2}|"
+
+ lines = list()
+ last_data = None
+ same_data = 0
+
+ for i in itertools.count(): # pragma: no branch
+ assert i < 0x10000
+ try:
+ this_data = self.read_without_encryption([sc], [BlockCode(i)])
+ except Type3TagCommandError:
+ i = i - 1
+ break
+
+ if this_data == last_data:
+ same_data += 1
+ else:
+ if same_data > 1:
+ lines.append(lprint(same_line_fmt, last_data, "*"))
+ lines.append(lprint(data_line_fmt, this_data, i))
+ last_data = this_data
+ same_data = 0
+
+ if same_data > 1:
+ lines.append(lprint(same_line_fmt, last_data, "*"))
+ if same_data > 0:
+ lines.append(lprint(data_line_fmt, this_data, i))
+
+ return lines
+
+ def format(self, version=None, wipe=None):
+ """Format and blank an NFC Forum Type 3 Tag.
+
+ A generic NFC Forum Type 3 Tag can be (re)formatted if it is
+ in either one of blank, initialized or readwrite state. By
+ formatting, all contents of the attribute information block is
+ overwritten with values determined. The number of user data
+ blocks is determined by reading all memory until an error
+ response. Similarily, the maximum number of data block that
+ can be read or written with a single command is determined by
+ sending successively increased read and write commands. The
+ current data length is set to zero. The NDEF mapping version
+ is set to the latest known version number (1.0), unless the
+ *version* argument is provided and it's major version number
+ corresponds to one of the known major version numbers.
+
+ By default, no data other than the attribute block is
+ modified. To overwrite user data the *wipe* argument must be
+ set to an integer value. The lower 8 bits of that value are
+ written to all data bytes that follow the attribute block.
+
+ """
+ return super(Type3Tag, self).format(version, wipe)
+
+ def _format(self, version, wipe):
+ assert version is None or type(version) is int
+ assert wipe is None or type(wipe) is int
+
+ if self.sys != 0x12FC:
+ log.warning("not an ndef tag and can not be made compatible")
+ return False
+ if version and version >> 4 != 1:
+ log.warning("Type 3 Tag NDEF mapping major version must be 1")
+ return False
+
+ try:
+ self.read_from_ndef_service(0)
+ except Type3TagCommandError:
+ log.warning("this tag does not have any usable data blocks")
+ return False
+
+ # To determine the total number of data blocks we start with
+ # the assumption that it must be between 0 and 2**16, then try
+ # reading in the middle and adjust the range depending on
+ # whether the read was successful or not. So in each round we
+ # have the smallest number that worked and the largest number
+ # that didn't, obviously the end is when that difference is 1.
+ """
+ nmaxb = [0, 0x10000]
+ while nmaxb[1] - nmaxb[0] > 1:
+ block = nmaxb[0] + (nmaxb[1] - nmaxb[0]) // 2 - 1
+ try:
+ self.read_from_ndef_service(block)
+ except Type3TagCommandError:
+ nmaxb[1] = block + 1
+ else:
+ nmaxb[0] = block + 1
+ """
+ nmaxb = [0, 0x10000]
+ while nmaxb[1] - nmaxb[0] > 1:
+ print(nmaxb)
+ block = nmaxb[0] + (nmaxb[1] - nmaxb[0]) // 2
+ try:
+ self.read_from_ndef_service(block)
+ except Type3TagCommandError:
+ nmaxb[1] = block
+ else:
+ nmaxb[0] = block
+
+ nmaxb = nmaxb[0]
+
+ # To get the number of blocks that can be read in one command
+ # we just try to read with an increasing number of blocks.
+ for nbr in range(1, 16):
+ try:
+ self.read_from_ndef_service(*(nbr*[0]))
+ except Type3TagCommandError:
+ nbr -= 1
+ break
+
+ # To get the number of blocks that can be written in one
+ # command we do essentially the same as for nbr, just that to
+ # preserve existing data we first read and then write it back.
+ data = self.read_from_ndef_service(0)
+ for nbw in range(1, 14):
+ try:
+ self.write_to_ndef_service(nbw*data, *(nbw*[0]))
+ except Type3TagCommandError:
+ nbw -= 1
+ break
+
+ # Tags with more than 4K memory require 3-byte block number
+ # format. This reduces the maximum number of blocks in write.
+ if nbw == 13 and nmaxb > 255:
+ nbw = 12
+
+ # We now have all information needed to create and write the
+ # new attribute data to block number 0.
+ attribute_data = bytearray(16)
+ attribute_data[0:5] = pack(">BBBH", version, nbr, nbw, nmaxb)
+ attribute_data[10] = 0x01 if nbw > 0 else 0x00
+ attribute_data[14:16] = pack(">H", sum(attribute_data[0:14]))
+ log.debug("set ndef attributes %s", hexlify(attribute_data).decode())
+ self.write_to_ndef_service(attribute_data, 0)
+
+ # If required, we will also overwrite the memory with the
+ # 8-bit integer provided. This could take a while.
+ if wipe is not None:
+ data = bytearray([wipe]) * 16
+ while nmaxb > 0:
+ self.write_to_ndef_service(data, nmaxb)
+ nmaxb = nmaxb - 1
+
+ return True
+
+ def polling(self, system_code=0xffff, request_code=0, time_slots=0):
+ """Aquire and identify a card.
+
+ The Polling command is used to detect the Type 3 Tags in the
+ field. It is also used for initialization and anti-collision.
+
+ The *system_code* identifies the card system to acquire. A
+ card can have multiple systems. The first system that matches
+ *system_code* will be activated. A value of 0xff for any of
+ the two bytes works as a wildcard, thus 0xffff activates the
+ very first system in the card. The card identification data
+ returned are the Manufacture ID (IDm) and Manufacture
+ Parameter (PMm).
+
+ The *request_code* tells the card whether it should return
+ additional information. The default value 0 requests no
+ additional information. Request code 1 means that the card
+ shall also return the system code, so polling for system code
+ 0xffff with request code 1 can be used to identify the first
+ system on the card. Request code 2 asks for communication
+ performance data, more precisely a bitmap of possible
+ communication speeds. Not all cards provide that information.
+
+ The number of *time_slots* determines whether there's a chance
+ to receive a response if multiple Type 3 Tags are in the
+ field. For the reader the number of time slots determines the
+ amount of time to wait for a response. Any Type 3 Tag in the
+ field, i.e. powered by the field, will choose a random time
+ slot to respond. With the default *time_slots* value 0 there
+ will only be one time slot available for all responses and
+ multiple responses would produce a collision. More time slots
+ reduce the chance of collisions (but may result in an
+ application working with a tag that was just accidentially
+ close enough). Only specific values should be used for
+ *time_slots*, those are 0, 1, 3, 7, and 15. Other values may
+ produce unexpected results depending on the tag product.
+
+ :meth:`polling` returns either the tuple (IDm, PMm) or the
+ tuple (IDm, PMm, *additional information*) depending on the
+ response lengt, all as bytearrays.
+
+ Command execution errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+
+ log.debug("polling for system 0x{0:04x}".format(system_code))
+ if time_slots not in (0, 1, 3, 7, 15):
+ log.debug("invalid number of time slots: {0}".format(time_slots))
+ raise ValueError("invalid number of time slots")
+ if request_code not in (0, 1, 2):
+ log.debug("invalid request code value: {0}".format(request_code))
+ raise ValueError("invalid request code for polling")
+
+ timeout = 0.003625 + time_slots * 0.001208
+ data = pack(">HBB", system_code, request_code, time_slots)
+ data = self.send_cmd_recv_rsp(0x00, data, timeout, send_idm=False)
+ if len(data) != (16 if request_code == 0 else 18):
+ log.debug("unexpected polling response length")
+ raise Type3TagCommandError(DATA_SIZE_ERROR)
+
+ return (data[0:8], data[8:16]) if len(data) == 16 else \
+ (data[0:8], data[8:16], data[16:18])
+
+ def read_without_encryption(self, service_list, block_list):
+ """Read data blocks from unencrypted services.
+
+ This method sends a Read Without Encryption command to the
+ tag. The data blocks to read are indicated by a sequence of
+ :class:`~nfc.tag.tt3.BlockCode` objects in *block_list*. Each
+ block code must reference a :class:`~nfc.tag.tt3.ServiceCode`
+ object from the iterable *service_list*. If any of the blocks
+ and services do not exist, the tag will stop processing at
+ that point and return a two byte error status. The status
+ bytes become the :attr:`~nfc.tag.TagCommandError.errno` value
+ of the :exc:`~nfc.tag.TagCommandError` exception.
+
+ As an example, the following code reads block 5 from service
+ 16 (service type 'random read-write w/o key') and blocks 0 to
+ 1 from service 80 (service type 'random read-only w/o key')::
+
+ sc1 = nfc.tag.tt3.ServiceCode(16, 0x09)
+ sc2 = nfc.tag.tt3.ServiceCode(80, 0x0B)
+ bc1 = nfc.tag.tt3.BlockCode(5, service=0)
+ bc2 = nfc.tag.tt3.BlockCode(0, service=1)
+ bc3 = nfc.tag.tt3.BlockCode(1, service=1)
+ try:
+ data = tag.read_without_encryption([sc1, sc2], [bc1, bc2, bc3])
+ except nfc.tag.TagCommandError as e:
+ if e.errno > 0x00FF:
+ print("the tag returned an error status")
+ else:
+ print("command failed with some other error")
+
+ Command execution errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+ a, b, e = self.pmm[5] & 7, self.pmm[5] >> 3 & 7, self.pmm[5] >> 6
+ timeout = 302.1E-6 * ((b + 1) * len(block_list) + a + 1) * 4**e
+
+ data = bytearray([
+ len(service_list)]) \
+ + b''.join([sc.pack() for sc in service_list]) \
+ + bytearray([len(block_list)]) \
+ + b''.join([bc.pack() for bc in block_list])
+
+ log.debug("read w/o encryption service/block list: {0} / {1}".format(
+ ' '.join([hexlify(sc.pack()).decode() for sc in service_list]),
+ ' '.join([hexlify(bc.pack()).decode() for bc in block_list])))
+
+ data = self.send_cmd_recv_rsp(0x06, data, timeout)
+
+ if len(data) != 1 + len(block_list) * 16:
+ log.debug("insufficient data received from tag")
+ raise Type3TagCommandError(DATA_SIZE_ERROR)
+
+ return data[1:]
+
+ def read_from_ndef_service(self, *blocks):
+ """Read block data from an NDEF compatible tag.
+
+ This is a convinience method to read block data from a tag
+ that has system code 0x12FC (NDEF). For other tags this method
+ simply returns :const:`None`. All arguments are block numbers
+ to read. To actually pass a list of block numbers requires
+ unpacking. The following example calls would have the same
+ effect of reading 32 byte data from from blocks 1 and 8.::
+
+ data = tag.read_from_ndef_service(1, 8)
+ data = tag.read_from_ndef_service(*list(1, 8))
+
+ Command execution errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+ if self.sys == 0x12FC:
+ sc_list = [ServiceCode(0, 0b001011)]
+ bc_list = [BlockCode(n) for n in blocks]
+ return self.read_without_encryption(sc_list, bc_list)
+
+ def write_without_encryption(self, service_list, block_list, data):
+ """Write data blocks to unencrypted services.
+
+ This method sends a Write Without Encryption command to the
+ tag. The data blocks to overwrite are indicated by a sequence
+ of :class:`~nfc.tag.tt3.BlockCode` objects in the parameter
+ *block_list*. Each block code must reference one of the
+ :class:`~nfc.tag.tt3.ServiceCode` objects in the iterable
+ *service_list*. If any of the blocks or services do not exist,
+ the tag will stop processing at that point and return a two
+ byte error status. The status bytes become the
+ :attr:`~nfc.tag.TagCommandError.errno` value of the
+ :exc:`~nfc.tag.TagCommandError` exception. The *data* to write
+ must be a byte string or array of length ``16 *
+ len(block_list)``.
+
+ As an example, the following code writes ``16 * "\\xAA"`` to
+ block 5 of service 16, ``16 * "\\xBB"`` to block 0 of service
+ 80 and ``16 * "\\xCC"`` to block 1 of service 80 (all services
+ are writeable without key)::
+
+ sc1 = nfc.tag.tt3.ServiceCode(16, 0x09)
+ sc2 = nfc.tag.tt3.ServiceCode(80, 0x09)
+ bc1 = nfc.tag.tt3.BlockCode(5, service=0)
+ bc2 = nfc.tag.tt3.BlockCode(0, service=1)
+ bc3 = nfc.tag.tt3.BlockCode(1, service=1)
+ sc_list = [sc1, sc2]
+ bc_list = [bc1, bc2, bc3]
+ data = 16 * "\\xAA" + 16 * "\\xBB" + 16 * "\\xCC"
+ try:
+ data = tag.write_without_encryption(sc_list, bc_list, data)
+ except nfc.tag.TagCommandError as e:
+ if e.errno > 0x00FF:
+ print("the tag returned an error status")
+ else:
+ print("command failed with some other error")
+
+ Command execution errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+ a, b, e = self.pmm[6] & 7, self.pmm[6] >> 3 & 7, self.pmm[6] >> 6
+ timeout = 302.1E-6 * ((b + 1) * len(block_list) + a + 1) * 4**e
+
+ data = bytearray([
+ len(service_list)]) \
+ + b"".join([sc.pack() for sc in service_list]) \
+ + bytearray([len(block_list)]) \
+ + b"".join([bc.pack() for bc in block_list]) \
+ + bytearray(data)
+
+ log.debug("write w/o encryption service/block list: {0} / {1}".format(
+ ' '.join([hexlify(sc.pack()).decode() for sc in service_list]),
+ ' '.join([hexlify(bc.pack()).decode() for bc in block_list])))
+
+ self.send_cmd_recv_rsp(0x08, data, timeout)
+
+ def write_to_ndef_service(self, data, *blocks):
+ """Write block data to an NDEF compatible tag.
+
+ This is a convinience method to write block data to a tag that
+ has system code 0x12FC (NDEF). For other tags this method
+ simply does nothing. The *data* to write must be a string or
+ bytearray with length equal ``16 * len(blocks)``. All
+ parameters following *data* are interpreted as block numbers
+ to write. To actually pass a list of block numbers requires
+ unpacking. The following example calls would have the same
+ effect of writing 32 byte zeros into blocks 1 and 8.::
+
+ tag.write_to_ndef_service(32 * "\\0", 1, 8)
+ tag.write_to_ndef_service(32 * "\\0", *list(1, 8))
+
+ Command execution errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+ if self.sys == 0x12FC:
+ sc_list = [ServiceCode(0, 0b001001)]
+ bc_list = [BlockCode(n) for n in blocks]
+ self.write_without_encryption(sc_list, bc_list, data)
+
+ def send_cmd_recv_rsp(self, cmd_code, cmd_data, timeout,
+ send_idm=True, check_status=True):
+ """Send a command and receive a response.
+
+ This low level method sends an arbitrary command with the
+ 8-bit integer *cmd_code*, followed by the captured tag
+ identifier (IDm) if *send_idm* is :const:`True` and the byte
+ string or bytearray *cmd_data*. It then waits *timeout*
+ seconds for a response, verifies that the response is
+ correctly formatted and, if *check_status* is :const:`True`,
+ that the status flags do not indicate an error.
+
+ All errors raise a :exc:`~nfc.tag.TagCommandError`
+ exception. Errors from response status flags produce an
+ :attr:`~nfc.tag.TagCommandError.errno` that is greater than
+ 255, all other errors are below 256.
+
+ """
+ idm = self.idm if send_idm else bytearray()
+ cmd = bytearray([2+len(idm)+len(cmd_data), cmd_code]) + idm + cmd_data
+ log.debug(">> {0:02x} {1:02x} {2} {3} ({4}s)".format(
+ cmd[0], cmd[1], hexlify(cmd[2:10]).decode(),
+ hexlify(cmd[10:]).decode(), timeout))
+
+ started = time.time()
+ error = None
+ for retry in range(3):
+ try:
+ rsp = self.clf.exchange(cmd, timeout)
+ break
+ except nfc.clf.CommunicationError as e:
+ error = e
+ reason = error.__class__.__name__
+ log.debug("%s after %d retries" % (reason, retry))
+ else:
+ if type(error) is nfc.clf.TimeoutError:
+ raise Type3TagCommandError(nfc.tag.TIMEOUT_ERROR)
+ if type(error) is nfc.clf.TransmissionError:
+ raise Type3TagCommandError(nfc.tag.RECEIVE_ERROR)
+ if type(error) is nfc.clf.ProtocolError: # pragma: no branch
+ raise Type3TagCommandError(nfc.tag.PROTOCOL_ERROR)
+
+ if rsp[0] != len(rsp):
+ log.debug("incorrect response length {0:02x}".format(rsp[0]))
+ raise Type3TagCommandError(RSP_LENGTH_ERROR)
+ if rsp[1] != cmd_code + 1:
+ log.debug("incorrect response code {0:02x}".format(rsp[1]))
+ raise Type3TagCommandError(RSP_CODE_ERROR)
+ if send_idm and rsp[2:10] != self.idm:
+ log.debug("wrong tag or transaction id {}".format(
+ hexlify(rsp[2:10]).decode()))
+ raise Type3TagCommandError(TAG_IDM_ERROR)
+ if not send_idm:
+ log.debug("<< {0:02x} {1:02x} {2}".format(
+ rsp[0], rsp[1], hexlify(rsp[2:]).decode()))
+ return rsp[2:]
+ if check_status and rsp[10] != 0:
+ log.debug("tag returned error status {}".format(
+ hexlify(rsp[10:12]).decode()))
+ raise Type3TagCommandError(unpack(">H", rsp[10:12])[0])
+ if not check_status:
+ log.debug("<< {0:02x} {1:02x} {2} {3}".format(
+ rsp[0], rsp[1], hexlify(rsp[2:10]).decode(),
+ hexlify(rsp[10:]).decode()))
+ return rsp[10:]
+ log.debug("<< {0:02x} {1:02x} {2} {3} {4} ({elapsed:f}s)".format(
+ rsp[0], rsp[1], hexlify(rsp[2:10]).decode(),
+ hexlify(rsp[10:12]).decode(), hexlify(rsp[12:]).decode(),
+ elapsed=time.time()-started))
+ return rsp[12:]
+
+
+class Type3TagEmulation(nfc.tag.TagEmulation):
+ """Framework for Type 3 Tag emulation.
+
+ """
+ def __init__(self, clf, target):
+ self.services = dict()
+ self.target = target
+ self.cmd = bytearray([len(target.tt3_cmd)+1]) + target.tt3_cmd
+ self.idm = target.sensf_res[1:9]
+ self.pmm = target.sensf_res[9:17]
+ self.sys = target.sensf_res[17:19]
+ self.clf = clf
+
+ def __str__(self):
+ """x.__str__() <==> str(x)"""
+ return "Type3TagEmulation IDm={id} PMm={pmm} SYS={sys}".format(
+ id=hexlify(self.idm).decode(),
+ pmm=hexlify(self.pmm).decode(),
+ sys=hexlify(self.sys).decode())
+
+ def add_service(self, service_code, block_read_func, block_write_func):
+ def default_block_read(block_number, rb, re):
+ return None
+
+ def default_block_write(block_number, block_data, wb, we):
+ return False
+
+ if block_read_func is None:
+ block_read_func = default_block_read
+
+ if block_write_func is None:
+ block_write_func = default_block_write
+
+ self.services[service_code] = (block_read_func, block_write_func)
+
+ def process_command(self, cmd):
+ log.debug("cmd: %s", hexlify(cmd).decode() if cmd else str(cmd))
+ if len(cmd) != cmd[0]:
+ log.error("tt3 command length error")
+ return None
+ if tuple(cmd[0:4]) in [(6, 0, 255, 255), (6, 0) + tuple(self.sys)]:
+ log.debug("process 'polling' command")
+ rsp = self.polling(cmd[2:])
+ return bytearray([2 + len(rsp), 0x01]) + rsp
+ if cmd[2:10] == self.idm:
+ if cmd[1] == 0x04:
+ log.debug("process 'request response' command")
+ rsp = self.request_response(cmd[10:])
+ return bytearray([10 + len(rsp), 0x05]) + self.idm + rsp
+ if cmd[1] == 0x06:
+ log.debug("process 'read without encryption' command")
+ rsp = self.read_without_encryption(cmd[10:])
+ return bytearray([10 + len(rsp), 0x07]) + self.idm + rsp
+ if cmd[1] == 0x08:
+ log.debug("process 'write without encryption' command")
+ rsp = self.write_without_encryption(cmd[10:])
+ return bytearray([10 + len(rsp), 0x09]) + self.idm + rsp
+ if cmd[1] == 0x0C:
+ log.debug("process 'request system code' command")
+ rsp = self.request_system_code(cmd[10:])
+ return bytearray([10 + len(rsp), 0x0D]) + self.idm + rsp
+
+ def send_response(self, rsp, timeout):
+ log.debug("rsp: {}".format(hexlify(rsp).decode()
+ if rsp is not None
+ else 'None'))
+ return self.clf.exchange(rsp, timeout)
+
+ def polling(self, cmd_data):
+ if cmd_data[2] == 1:
+ rsp = self.idm + self.pmm + self.sys
+ else:
+ rsp = self.idm + self.pmm
+ return rsp
+
+ def request_response(self, cmd_data):
+ return bytearray([0])
+
+ def read_without_encryption(self, cmd_data):
+ service_list = cmd_data.pop(0) * [[None, None]]
+ for i in range(len(service_list)):
+ service_code = cmd_data[1] << 8 | cmd_data[0]
+ if service_code not in self.services.keys():
+ return bytearray([0xFF, 0xA1])
+ service_list[i] = [service_code, 0]
+ del cmd_data[0:2]
+
+ service_block_list = cmd_data.pop(0) * [None]
+ if len(service_block_list) > 15:
+ return bytearray([0xFF, 0xA2])
+ for i in range(len(service_block_list)):
+ try:
+ service_list_item = service_list[cmd_data[0] & 0x0F]
+ service_code = service_list_item[0]
+ service_list_item[1] += 1
+ except IndexError:
+ return bytearray([1 << (i % 8), 0xA3])
+ if cmd_data[0] >= 128:
+ block_number = cmd_data[1]
+ del cmd_data[0:2]
+ else:
+ block_number = cmd_data[2] << 8 | cmd_data[1]
+ del cmd_data[0:3]
+ service_block_list[i] = [service_code, block_number, 0]
+
+ service_block_count = dict(service_list)
+ for service_block_list_item in service_block_list:
+ service_code = service_block_list_item[0]
+ service_block_list_item[2] = service_block_count[service_code]
+
+ block_data = bytearray()
+ for i, service_block_list_item in enumerate(service_block_list):
+ service_code, block_number, block_count = service_block_list_item
+ # rb (read begin) and re (read end) mark an atomic read
+ rb = bool(block_count == service_block_count[service_code])
+ service_block_count[service_code] -= 1
+ re = bool(service_block_count[service_code] == 0)
+ read_func, write_func = self.services[service_code]
+ one_block_data = read_func(block_number, rb, re)
+ if one_block_data is None:
+ return bytearray([1 << (i % 8), 0xA2])
+ block_data.extend(one_block_data)
+
+ return bytearray([0, 0, int(math.floor(len(block_data)/16))]) \
+ + block_data
+
+ def write_without_encryption(self, cmd_data):
+ service_list = cmd_data.pop(0) * [[None, None]]
+ for i in range(len(service_list)):
+ service_code = cmd_data[1] << 8 | cmd_data[0]
+ if service_code not in self.services.keys():
+ return bytearray([255, 0xA1])
+ service_list[i] = [service_code, 0]
+ del cmd_data[0:2]
+
+ service_block_list = cmd_data.pop(0) * [None]
+ for i in range(len(service_block_list)):
+ try:
+ service_list_item = service_list[cmd_data[0] & 0x0F]
+ service_code = service_list_item[0]
+ service_list_item[1] += 1
+ except IndexError:
+ return bytearray([1 << (i % 8), 0xA3])
+ if cmd_data[0] >= 128:
+ block_number = cmd_data[1]
+ del cmd_data[0:2]
+ else:
+ block_number = cmd_data[2] << 8 | cmd_data[1]
+ del cmd_data[0:3]
+ service_block_list[i] = [service_code, block_number, 0]
+
+ service_block_count = dict(service_list)
+ for service_block_list_item in service_block_list:
+ service_code = service_block_list_item[0]
+ service_block_list_item[2] = service_block_count[service_code]
+
+ block_data = cmd_data[0:]
+ if len(block_data) % 16 != 0:
+ return bytearray([255, 0xA2])
+
+ for i, service_block_list_item in enumerate(service_block_list):
+ service_code, block_number, block_count = service_block_list_item
+ # wb (write begin) and we (write end) mark an atomic write
+ wb = bool(block_count == service_block_count[service_code])
+ service_block_count[service_code] -= 1
+ we = bool(service_block_count[service_code] == 0)
+ read_func, write_func = self.services[service_code]
+ if not write_func(block_number, block_data[i*16:(i+1)*16], wb, we):
+ return bytearray([1 << (i % 8), 0xA2])
+
+ return bytearray([0, 0])
+
+ def request_system_code(self, cmd_data):
+ return b'\x01' + self.sys
+
+
+def activate(clf, target):
+ if not target.sensf_res[1:3] == b"\x01\xFE":
+ import nfc.tag.tt3_sony
+ tag = nfc.tag.tt3_sony.activate(clf, target)
+ return tag if tag else Type3Tag(clf, target)
diff --git a/src/lib/nfc/tag/tt3_sony.py b/src/lib/nfc/tag/tt3_sony.py
new file mode 100644
index 0000000..9bab877
--- /dev/null
+++ b/src/lib/nfc/tag/tt3_sony.py
@@ -0,0 +1,987 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2014, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import nfc.tag
+from . import tt3
+
+import os
+import struct
+from binascii import hexlify
+from pyDes import triple_des, CBC
+from struct import pack, unpack
+import itertools
+
+import logging
+log = logging.getLogger(__name__)
+
+
+def activate(clf, target):
+ # http://www.sony.net/Products/felica/business/tech-support/list.html
+ ic_code = target.sensf_res[10]
+ if ic_code in FelicaLite.IC_CODE_MAP.keys():
+ return FelicaLite(clf, target)
+ if ic_code in FelicaLiteS.IC_CODE_MAP.keys():
+ return FelicaLiteS(clf, target)
+ if ic_code in FelicaStandard.IC_CODE_MAP.keys():
+ return FelicaStandard(clf, target)
+ if ic_code in FelicaMobile.IC_CODE_MAP.keys():
+ return FelicaMobile(clf, target)
+ if ic_code in FelicaPlug.IC_CODE_MAP.keys():
+ return FelicaPlug(clf, target)
+ return None
+
+
+class FelicaStandard(tt3.Type3Tag):
+ """Standard FeliCa is a range of FeliCa OS based card products with a
+ flexible file system that supports multiple applications and
+ services on the same card. Services can individually be protected
+ with a card key and all communication with protected services is
+ encrypted.
+
+ """
+ IC_CODE_MAP = {
+ # IC IC-NAME NBR NBW
+ 0x00: ("RC-S830", 8, 8), # RC-S831/833
+ 0x01: ("RC-S915", 12, 8), # RC-S860/862/863/864/891
+ 0x02: ("RC-S919", 1, 1), # RC-S890
+ 0x08: ("RC-S952", 12, 8),
+ 0x09: ("RC-S953", 12, 8),
+ 0x0B: ("RC-S???", 1, 1), # new suica
+ 0x0C: ("RC-S954", 12, 8),
+ 0x0D: ("RC-S960", 12, 10), # RC-S880/889
+ 0x20: ("RC-S962", 12, 10), # RC-S885/888/892/893
+ 0x32: ("RC-SA00/1", 1, 1), # AES chip
+ 0x35: ("RC-SA00/2", 1, 1),
+ }
+
+ def __init__(self, clf, target):
+ super(FelicaStandard, self).__init__(clf, target)
+ self._product = "FeliCa Standard ({0})".format(
+ self.IC_CODE_MAP[self.pmm[1]][0])
+
+ def _is_present(self):
+ # Perform a presence check. Modern FeliCa cards implement the
+ # RequestResponse command, so we'll try that first. If it
+ # fails we resort the generic way that works for all type 3
+ # tags (but resets the card operating mode to zero).
+ try:
+ return self.request_response() in (0, 1, 2, 3)
+ except tt3.Type3TagCommandError:
+ return super(FelicaStandard, self)._is_present()
+
+ def dump(self):
+ # Dump the content of a FeliCa card as good as possible. This
+ # is unfortunately rather complex because we want to reflect
+ # the area structure with indentation and summarize overlapped
+ # services under a single item.
+
+ def print_system(system_code):
+ # Print system information
+ system_code_map = {
+ 0x0000: "SDK Sample",
+ 0x0003: "Suica",
+ 0x12FC: "NDEF",
+ 0x811D: "Edy",
+ 0x8620: "Blackboard",
+ 0xFE00: "Common Area",
+ }
+ return ["System {0:04X} ({1})".format(
+ system_code, system_code_map.get(system_code, 'unknown'))]
+
+ def print_area(area_from, area_last, depth):
+ # Prints area information with indentation.
+ return ["{indent}Area {0:04X}--{1:04X}".format(
+ area_from, area_last, indent=depth*' ')]
+
+ def print_service(services, depth):
+ # This function processes a list of overlapped services
+ # and reads all block data if there is one service that
+ # does not require a key. First we figure out the common
+ # service type and which access modes are available.
+ if services[0] >> 2 & 0b1111 == 0b0010:
+ service_type = "Random"
+ access_types = " & ".join([(
+ "write with key", "write w/o key",
+ "read with key", "read w/o key")[x & 3] for x in services])
+ if services[0] >> 2 & 0b1111 == 0b0011:
+ service_type = "Cyclic"
+ access_types = " & ".join([(
+ "write with key", "write w/o key",
+ "read with key", "read w/o key")[x & 3] for x in services])
+ if services[0] >> 2 & 0b1110 == 0b0100:
+ service_type = "Purse"
+ access_types = " & ".join([(
+ "direct with key", "direct w/o key",
+ "cashback with key", "cashback w/o key",
+ "decrement with key", "decrement w/o key",
+ "read with key", "read w/o key")[x & 7] for x in services])
+ # Now we print one line to verbosely describe the service
+ # and list the service codes.
+ service_codes = " ".join(["0x{0:04X}".format(x) for x in services])
+ lines = [
+ "{indent}{type} Service {number}: {access} ({0})".format(
+ service_codes, indent=depth*' ', type=service_type,
+ number=services[0] >> 6, access=access_types)]
+ # The final piece is to see if any of the services allows
+ # us to read block data without a key. Services w/o key
+ # have the last bit set to 1, so we generate a list of
+ # only those services and iterate over the slice from the
+ # last item to end (that's one or zero services).
+ for service in [sc for sc in services if sc & 1][-1:]:
+ sc = tt3.ServiceCode(service >> 6, service & 0b111111)
+ for line in self.dump_service(sc):
+ lines.append(depth*' ' + ' ' + line)
+ return lines
+
+ # Unfortunately there are some older cards with reduced
+ # command support. If request_system_code() is not supported
+ # we can only see if the current system code is NDEF and try
+ # to dup that, otherwise it is the end.
+ try:
+ card_system_codes = self.request_system_code()
+ except nfc.tag.TagCommandError:
+ if self.sys == 0x12FC:
+ return super(FelicaStandard, self).dump()
+ else:
+ return ["unable to create a memory dump"]
+
+ # A FeliCa card has one or more systems, each system has one
+ # or more areas which may be nested, and an area may have zero
+ # to many services. The outer loop iterates over all system
+ # codes that are present on the card. The inner loop iterates
+ # by index over all area and service definitions.
+ lines = []
+ for system_code in card_system_codes:
+
+ # A system must be activated first, this is what the
+ # polling() command does.
+ idm, pmm = self.polling(system_code)
+ self.idm = idm
+ self.pmm = pmm
+ self.sys = system_code
+ lines.extend(print_system(system_code))
+
+ area_stack = []
+ overlap_services = []
+
+ # Walk through the list of services by index. The first
+ # index for which there is no service returns None and
+ # terminate the loop.
+ for service_index in itertools.count(): # pragma: no branch
+ assert service_index < 0x10000
+ depth = len(area_stack)
+ area_or_service = self.search_service_code(service_index)
+ if area_or_service is None:
+ # Went beyond the service index. Print overlap
+ # services if any and exit loop.
+ if len(overlap_services) > 0:
+ lines.extend(print_service(overlap_services, depth))
+ overlap_services = []
+ break
+ elif len(area_or_service) == 1:
+ # Found a service definition. Add as overlap
+ # service if it is either the first or same type
+ # (Random, Cyclic, Purse) as the previous one. If
+ # it is different then print the current overlap
+ # services and remember this for the next round.
+ service = area_or_service[0]
+ end_overlap_services = False
+ if len(overlap_services) == 0:
+ overlap_services.append(service)
+ elif service >> 4 == overlap_services[-1] >> 4:
+ if service >> 4 & 1: # purse
+ overlap_services.append(service)
+ elif service >> 2 == overlap_services[-1] >> 2:
+ overlap_services.append(service)
+ else:
+ end_overlap_services = True
+ else:
+ end_overlap_services = True
+ if end_overlap_services:
+ lines.extend(print_service(overlap_services, depth))
+ overlap_services = [service]
+ elif len(area_or_service) == 2:
+ # Found an area definition. Print any services
+ # that we might so far have assembled, then
+ # process the area information.
+ if len(overlap_services) > 0:
+ lines.extend(print_service(overlap_services, depth))
+ overlap_services = []
+ area_from, area_last = area_or_service
+ if len(area_stack) > 0 and area_from > area_stack[-1][1]:
+ area_stack.pop()
+ lines.extend(print_area(area_from, area_last, depth))
+ area_stack.append((area_from, area_last))
+
+ return lines
+
+ def request_service(self, service_list):
+ """Verify existence of a service (or area) and get the key version.
+
+ Each service (or area) to verify must be given as a
+ :class:`~nfc.tag.tt3.ServiceCode` in the iterable
+ *service_list*. The key versions are returned as a list of
+ 16-bit integers, in the order requested. If a specified
+ service (or area) does not exist, the key version will be
+ 0xFFFF.
+
+ Command execution errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+ a, b, e = self.pmm[2] & 7, self.pmm[2] >> 3 & 7, self.pmm[2] >> 6
+ timeout = 302E-6 * ((b + 1) * len(service_list) + a + 1) * 4**e
+ pack = lambda x: x.pack() # noqa: E731
+ data = bytearray([len(service_list)]) \
+ + b''.join(map(pack, service_list))
+ data = self.send_cmd_recv_rsp(0x02, data, timeout, check_status=False)
+ if len(data) != 1 + len(service_list) * 2:
+ log.debug("insufficient data received from tag")
+ raise tt3.Type3TagCommandError(tt3.DATA_SIZE_ERROR)
+ return [unpack("> 3 & 7, self.pmm[3] >> 6
+ timeout = 302E-6 * (b + 1 + a + 1) * 4**e
+ data = self.send_cmd_recv_rsp(0x04, b'', timeout, check_status=False)
+ if len(data) != 1:
+ log.debug("insufficient data received from tag")
+ raise tt3.Type3TagCommandError(tt3.DATA_SIZE_ERROR)
+ return data[0] # mode
+
+ def search_service_code(self, service_index):
+ """Search for a service code that corresponds to an index.
+
+ The Search Service Code command provides access to the
+ iterable list of services and areas within the activated
+ system. The *service_index* argument may be any value from 0
+ to 0xffff. As long as there is a service or area found for a
+ given *service_index*, the information returned is a tuple
+ with either one or two 16-bit integer elements. Two integers
+ are returned for an area definition, the first is the area
+ code and the second is the largest possible service index for
+ the area. One integer, the service code, is returned for a
+ service definition. The return value is :const:`None` if the
+ *service_index* was not found.
+
+ For example, to print all services and areas of the active
+ system: ::
+
+ for i in xrange(0x10000):
+ area_or_service = tag.search_service_code(i)
+ if area_or_service is None:
+ break
+ elif len(area_or_service) == 1:
+ sc = area_or_service[0]
+ print(nfc.tag.tt3.ServiceCode(sc >> 6, sc & 0x3f))
+ elif len(area_or_service) == 2:
+ area_code, area_last = area_or_service
+ print("Area {0:04x}--{0:04x}".format(area_code, area_last))
+
+ Command execution errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+ log.debug("search service code index {0}".format(service_index))
+ # The maximum response time is given by the value of PMM[3].
+ # Some cards (like RC-S860 with IC RC-S915) encode a value
+ # that is too short, thus we use at lest 2 ms.
+ a, e = self.pmm[3] & 7, self.pmm[3] >> 6
+ timeout = max(302E-6 * (a + 1) * 4**e, 0.002)
+ data = pack("> 6
+ timeout = max(302E-6 * (a + 1) * 4**e, 0.002)
+ data = self.send_cmd_recv_rsp(0x0C, b'', timeout, check_status=False)
+ if len(data) != 1 + data[0] * 2:
+ log.debug("insufficient data received from tag")
+ raise tt3.Type3TagCommandError(tt3.DATA_SIZE_ERROR)
+ return [unpack(">H", data[i:i+2])[0] for i in range(1, len(data), 2)]
+
+
+class FelicaMobile(FelicaStandard):
+ """Mobile FeliCa is a modification of FeliCa for use in mobile
+ phones. This class does currently not implement anything specific
+ beyond recognition of the Mobile FeliCa OS version.
+
+ """
+ IC_CODE_MAP = {
+ # IC IC-NAME NBR NBW
+ 0x06: ("1.0", 1, 1),
+ 0x07: ("1.0", 1, 1),
+ 0x10: ("2.0", 1, 1),
+ 0x11: ("2.0", 1, 1),
+ 0x12: ("2.0", 1, 1),
+ 0x13: ("2.0", 1, 1),
+ 0x14: ("3.0", 1, 1),
+ 0x15: ("3.0", 1, 1),
+ 0x16: ("3.0", 1, 1),
+ 0x17: ("3.0", 1, 1),
+ 0x18: ("3.0", 1, 1),
+ 0x19: ("3.0", 1, 1),
+ 0x1A: ("3.0", 1, 1),
+ 0x1B: ("3.0", 1, 1),
+ 0x1C: ("3.0", 1, 1),
+ 0x1D: ("3.0", 1, 1),
+ 0x1E: ("3.0", 1, 1),
+ 0x1F: ("3.0", 1, 1),
+ }
+
+ def __init__(self, clf, target):
+ super(FelicaMobile, self).__init__(clf, target)
+ self._product = "FeliCa Mobile " + self.IC_CODE_MAP[self.pmm[1]][0]
+
+
+class FelicaLite(tt3.Type3Tag):
+ """FeliCa Lite is a version of FeliCa with simplified file system and
+ security functions. The usable memory is 13 blocks (one block has
+ 16 byte) plus a one block subtraction register. The tag can be
+ configured with a card key to authenticate the tag and protect
+ integrity of data reads.
+
+ """
+ IC_CODE_MAP = {
+ 0xF0: "FeliCa Lite (RC-S965)",
+ }
+
+ class NDEF(tt3.Type3Tag.NDEF):
+ def _read_attribute_data(self):
+ log.debug("FelicaLite.read_attribute_data")
+ attributes = super(FelicaLite.NDEF, self)._read_attribute_data()
+ if attributes is not None and self._tag.is_authenticated:
+ # when authenticated we need to make room for the mac
+ self._original_nbr = attributes['nbr']
+ attributes['nbr'] = min(attributes['nbr'], 3)
+ return attributes
+
+ def _write_attribute_data(self, attributes):
+ log.debug("FelicaLite.read_attribute_data")
+ if self._tag.is_authenticated:
+ attributes = attributes.copy()
+ attributes['nbr'] = self._original_nbr
+ super(FelicaLite.NDEF, self)._write_attribute_data(attributes)
+
+ def __init__(self, clf, target):
+ super(FelicaLite, self).__init__(clf, target)
+ self._product = self.IC_CODE_MAP[self.pmm[1]]
+ self._sk = self._iv = None
+ self.read_from_ndef_service = self.read_without_mac
+ self.write_to_ndef_service = self.write_without_mac
+
+ def dump(self):
+ def oprint(octets):
+ return ' '.join(['%02x' % x for x in octets])
+
+ def cprint(octets):
+ return ''.join([chr(x) if 32 <= x <= 126 else '.' for x in octets])
+
+ userblocks = list()
+ for i in range(0, 14):
+ try:
+ data = self.read_without_mac(i)
+ except tt3.Type3TagCommandError:
+ userblocks.append("{0} |{1}|".format(
+ " ".join(16 * ["??"]), 16*"."))
+ else:
+ userblocks.append("{0} |{1}|".format(
+ oprint(data), cprint(data)))
+
+ lines = list()
+ last_block = None
+ same_blocks = 0
+
+ for i, block in enumerate(userblocks):
+ if block == last_block:
+ same_blocks += 1
+ continue
+ if same_blocks:
+ if same_blocks > 1:
+ lines.append(" * " + last_block)
+ same_blocks = 0
+ lines.append("{0:3}: ".format(i) + block)
+ last_block = block
+
+ if same_blocks:
+ if same_blocks > 1:
+ lines.append(" * " + last_block)
+ lines.append("{0:3}: ".format(i) + block)
+
+ data = self.read_without_mac(14)
+ lines.append(" 14: {0} ({1})".format(oprint(data), "REGA[4]B[4]C[8]"))
+
+ text = ("RC1[8], RC2[8]", "MAC[8]", "IDD[8], DFC[2]",
+ "IDM[8], PMM[8]", "SERVICE_CODE[2]",
+ "SYSTEM_CODE[2]", "CKV[2]", "CK1[8], CK2[8]",
+ "MEMORY_CONFIG")
+ config = dict(zip(range(0x80, 0x80+len(text)), text))
+
+ for i in sorted(config.keys()):
+ try:
+ data = self.read_without_mac(i)
+ except tt3.Type3TagCommandError:
+ lines.append("{0:3}: {1}({2})".format(
+ i, 16 * "?? ", config[i]))
+ else:
+ lines.append("{0:3}: {1} ({2})".format(
+ i, oprint(data), config[i]))
+
+ return lines
+
+ @staticmethod
+ def generate_mac(data, key, iv, flip_key=False):
+ # Data is first split into tuples of 8 character bytes, each
+ # tuple then reversed and joined, finally all joined back to
+ # one string that is then triple des encrypted with key and
+ # initialization vector iv. If flip_key is True then the key
+ # halfs will be exchanged (this is used to generate a mac for
+ # write). The resulting mac is the last 8 bytes returned in
+ # reversed order.
+ assert len(data) % 8 == 0 and len(key) == 16 and len(iv) == 8
+ key = bytes(key[8:] + key[:8]) if flip_key else bytes(key)
+ txt = b''.join([
+ struct.pack("{}B".format(len(x)), *reversed(x))
+ if isinstance(x[0], int)
+ else b''.join(reversed(x))
+ for x in zip(*[iter(bytes(data))]*8)])
+ return bytearray(triple_des(key, CBC, bytes(iv)).encrypt(txt)[:-9:-1])
+
+ def protect(self, password=None, read_protect=False, protect_from=0):
+ """Protect a FeliCa Lite Tag.
+
+ A FeliCa Lite Tag can be provisioned with a custom password
+ (or the default manufacturer key if the password is an empty
+ string or bytearray) to ensure that data retrieved by future
+ read operations, after authentication, is genuine. Read
+ protection is not supported.
+
+ A non-empty *password* must provide at least 128 bit key
+ material, in other words it must be a string or bytearray of
+ length 16 or more.
+
+ The memory unit for the value of *protect_from* is 16 byte,
+ thus with ``protect_from=2`` bytes 0 to 31 are not protected.
+ If *protect_from* is zero (the default value) and the Tag has
+ valid NDEF management data, the NDEF RW Flag is set to read
+ only.
+
+ """
+ return super(FelicaLite, self).protect(
+ password, read_protect, protect_from)
+
+ def _protect(self, password, read_protect, protect_from):
+ if password and len(password) < 16:
+ raise ValueError("password must be at least 16 byte")
+
+ if protect_from < 0:
+ raise ValueError("protect_from can not be negative")
+
+ if read_protect:
+ log.info("this tag can not be made read protected")
+ return False
+
+ # The memory configuration block contains access permissions
+ # and ndef compatibility information.
+ mc = self.read_without_mac(0x88)
+
+ if password is not None:
+ if mc[2] != 0xFF:
+ log.info("system block protected, can't write key")
+ return False
+
+ # if password is empty use factory key of 16 zero bytes
+ key = password[0:16] if password else b"\0"*16
+
+ log.debug("protect with key %s", hexlify(key).decode())
+ self.write_without_mac(key[7::-1] + key[15:7:-1], 0x87)
+
+ if protect_from < 14:
+ log.debug("write protect blocks {0}--13".format(protect_from))
+ mc[0:2] = pack("H', sum(attribute_data[0:14]))
+ self.write_without_mac(attribute_data, 0)
+
+ log.debug("write protect system blocks 82,83,84,86,87")
+ mc[2] = 0x00 # set system blocks 82,83,84,86,87 to read only
+
+ log.debug("write memory configuration %s", hexlify(mc).decode())
+ self.write_without_mac(mc, 0x88)
+ return True
+
+ def authenticate(self, password):
+ """Authenticate a FeliCa Lite Tag.
+
+ A FeliCa Lite Tag is authenticated by a procedure that allows
+ both the reader and the tag to calculate a session key from a
+ random challenge send by the reader and a key that is securely
+ stored on the tag and provided to :meth:`authenticate` as the
+ *password* argument. If the tag was protected with an earlier
+ call to :meth:`protect` then the same password should
+ successfully authenticate.
+
+ After authentication the :meth:`read_with_mac` method can be
+ used to read data such that it can not be falsified on
+ transmission.
+
+ """
+ return super(FelicaLite, self).authenticate(password)
+
+ def _authenticate(self, password):
+ if password and len(password) < 16:
+ raise ValueError("password must be at least 16 byte")
+
+ # Perform internal authentication, i.e. ensure that the tag
+ # has the same card key as in password. If the password is
+ # empty, we'll try with the factory key.
+ key = b"\0" * 16 if not password else password[0:16]
+
+ log.debug("authenticate with key {}".format(hexlify(key).decode()))
+ self._authenticated = False
+ self.read_from_ndef_service = self.read_without_mac
+ self.write_to_ndef_service = self.write_without_mac
+
+ # Internal authentication starts with a random challenge (rc1 || rc2)
+ # that we write to the rc block. Because the tag works little endian,
+ # we reverse the order of rc1 and rc2 bytes when writing.
+ rc = os.urandom(16)
+ log.debug("rc1 = {}".format(hexlify(rc[:8]).decode()))
+ log.debug("rc2 = {}".format(hexlify(rc[8:]).decode()))
+ self.write_without_mac(rc[7::-1] + rc[15:7:-1], 0x80)
+
+ # The session key becomes the triple_des encryption of the random
+ # challenge under the card key and with an initialization vector of
+ # all zero.
+ sk = triple_des(key, CBC, b'\00' * 8).encrypt(rc)
+ log.debug("sk1 = {}".format(hexlify(sk[:8]).decode()))
+ log.debug("sk2 = {}".format(hexlify(sk[8:]).decode()))
+
+ # By reading the id and mac block together we get the mac that the
+ # tag has generated over the id block data under it's session key
+ # generated the same way as we did) and with rc1 as the
+ # initialization vector.
+ data = self.read_without_mac(0x82, 0x81)
+
+ # Now we check if we calculate the same mac with our session key.
+ # Note that, because of endianess, data must be reversed in chunks
+ # of 8 bytes as does the 8 byte mac - this is all done within the
+ # generate_mac() function.
+ if data[-16:-8] == self.generate_mac(data[0:-16], sk, iv=rc[0:8]):
+ log.debug("tag authentication completed")
+ self._sk = sk
+ self._iv = rc[0:8]
+ self._authenticated = True
+ self.read_from_ndef_service = self.read_with_mac
+ else:
+ log.debug("tag authentication failed")
+
+ return self._authenticated
+
+ def format(self, version=0x10, wipe=None):
+ """Format a FeliCa Lite Tag for NDEF.
+
+ """
+ return super(FelicaLite, self).format(version, wipe)
+
+ def _format(self, version, wipe):
+ assert type(version) is int
+ assert wipe is None or type(wipe) is int
+
+ if version and version >> 4 != 1:
+ log.error("type 3 tag ndef mapping major version must be 1")
+ return False
+
+ # The memory configuration block contains access permissions
+ # and ndef compatibility information.
+ mc = self.read_without_mac(0x88)
+
+ if mc[0] & 0x01 != 0x01:
+ log.info("the first user data block is not writeable")
+ return False
+
+ if not mc[3] & 0x01: # ndef compatibility flag
+ if mc[2] == 0xFF: # mc block is writeable
+ mc[3] = mc[3] | 0x01
+ self.write_without_mac(mc, 0x88)
+ else:
+ log.info("this tag can no longer be changed to ndef")
+ return False
+
+ # Count the number of writeable data blocks (that is excluding
+ # the attribute block) from the least significant read/write
+ # permission bits that are consecutively set to 1.
+ rw_bits = unpack("> (nmaxb + 1) & 1 == 0:
+ break
+
+ # Create and write the attribute data. Version number, Nbr and
+ # Nbw are fix and we have just determined Nmaxb.
+ attribute_data = bytearray(16)
+ attribute_data[:14] = pack(">BBBHxxxxxBxxx", version, 4, 1, nmaxb, 1)
+ attribute_data[14:] = pack(">H", sum(attribute_data[:14]))
+ log.debug("set ndef attributes %s", hexlify(attribute_data).decode())
+ self.write_without_mac(attribute_data, 0)
+
+ # Overwrite the ndef message area if a wipe is requested.
+ if wipe is not None:
+ data = bytearray(16 * [wipe])
+ for block in range(1, nmaxb+1):
+ self.write_without_mac(data, block)
+
+ return True
+
+ def read_without_mac(self, *blocks):
+ """Read a number of data blocks without integrity check.
+
+ This method accepts a variable number of integer arguments as
+ the block numbers to read. The blocks are read with service
+ code 0x000B (NDEF).
+
+ Tag command errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+ log.debug("read {0} block(s) without mac".format(len(blocks)))
+ service_list = [tt3.ServiceCode(0, 0b001011)]
+ block_list = [tt3.BlockCode(n) for n in blocks]
+ return self.read_without_encryption(service_list, block_list)
+
+ def read_with_mac(self, *blocks):
+ """Read a number of data blocks with integrity check.
+
+ This method accepts a variable number of integer arguments as
+ the block numbers to read. The blocks are read with service
+ code 0x000B (NDEF). Along with the requested block data the
+ tag returns a message authentication code that is verified
+ before data is returned. If verification fails the return
+ value of :meth:`read_with_mac` is None.
+
+ A :exc:`RuntimeError` exception is raised if the tag was not
+ authenticated before calling this method.
+
+ Tag command errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+ log.debug("read {0} block(s) with mac".format(len(blocks)))
+
+ if self._sk is None or self._iv is None:
+ raise RuntimeError("authentication required")
+
+ service_list = [tt3.ServiceCode(0, 0b001011)]
+ block_list = [tt3.BlockCode(n) for n in blocks]
+ block_list.append(tt3.BlockCode(0x81))
+
+ data = self.read_without_encryption(service_list, block_list)
+ data, mac = data[0:-16], data[-16:-8]
+ if mac != self.generate_mac(data, self._sk, self._iv):
+ log.warning("mac verification failed")
+ else:
+ return data
+
+ def write_without_mac(self, data, block):
+ """Write a data block without integrity check.
+
+ This is the standard write method for a FeliCa Lite. The
+ 16-byte string or bytearray *data* is written to the numbered
+ *block* in service 0x0009 (NDEF write service). ::
+
+ data = bytearray(range(16)) # 0x00, 0x01, ... 0x0F
+ try: tag.write_without_mac(data, 5) # write block 5
+ except nfc.tag.TagCommandError:
+ print("something went wrong")
+
+ Tag command errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+ # Write a single data block without a mac. Write with mac is
+ # only supported by FeliCa Lite-S.
+ assert len(data) == 16 and type(block) is int
+ log.debug("write 1 block without mac".format())
+ sc_list = [tt3.ServiceCode(0, 0b001001)]
+ bc_list = [tt3.BlockCode(block)]
+ self.write_without_encryption(sc_list, bc_list, data)
+
+
+class FelicaLiteS(FelicaLite):
+ """FeliCa Lite-S is a version of FeliCa Lite with enhanced security
+ functions. It provides mutual authentication were both the tag and
+ the reader must demonstrate posession of the card key before data
+ writes can be made. It is also possible to require mutual
+ authentication for data reads.
+
+ """
+ IC_CODE_MAP = {
+ 0xF1: "FeliCa Lite-S (RC-S966)",
+ 0xF2: "FeliCa Link (RC-S730) Lite-S Mode",
+ }
+
+ class NDEF(FelicaLite.NDEF):
+ def _read_attribute_data(self):
+ log.debug("FelicaLiteS.read_attribute_data")
+ attributes = super(FelicaLiteS.NDEF, self)._read_attribute_data()
+ if attributes is not None and self._tag._authenticated:
+ # when authenticated and user data is writeable
+ mc = self._tag.read_without_mac(0x88)
+ rw_bits = unpack("H', sum(attribute_data[0:14]))
+ self.write_without_mac(attribute_data, 0)
+
+ log.debug("write protect system blocks 82,83,84,86,87")
+ mc[2] = 0x00 # set system blocks 82,83,84,86,87 to read only
+ mc[5] = 0x01 # but allow write with mac to ck and ckv block
+
+ # Write the new memory control block.
+ log.debug("write memory configuration %s", hexlify(mc).decode())
+ self.write_without_mac(mc, 0x88)
+ return True
+
+ def authenticate(self, password):
+ """Mutually authenticate with a FeliCa Lite-S Tag.
+
+ FeliCa Lite-S supports enhanced security functions, one of
+ them is the mutual authentication performed by this
+ method. The first part of mutual authentication is to
+ authenticate the tag with :meth:`FelicaLite.authenticate`. If
+ successful, the shared session key is used to generate the
+ integrity check value for write operation to update a specific
+ memory block. If that was successful then the tag is ensured
+ that the reader has the correct card key.
+
+ After successful authentication the
+ :meth:`~FelicaLite.read_with_mac` and :meth:`write_with_mac`
+ methods can be used to read and write data such that it can
+ not be falsified on transmission.
+
+ """
+ if super(FelicaLiteS, self).authenticate(password):
+ # At this point we have achieved internal authentication,
+ # i.e we know that the tag has the same card key as in
+ # password. We now reset the authentication status and do
+ # external authentication to assure the tag that we have
+ # the right card key.
+ self._authenticated = False
+ self.read_from_ndef_service = self.read_without_mac
+ self.write_to_ndef_service = self.write_without_mac
+
+ # To authenticate to the tag we write a 01h into the
+ # ext_auth byte of the state block (block 0x92). The other
+ # bytes of the state block can be all set to zero.
+ self.write_with_mac(b"\x01" + 15*b"\0", 0x92)
+
+ # Now read the state block and check the value of the
+ # ext_auth to see if we are authenticated. If it's 01h
+ # then we are, otherwise not.
+ if self.read_with_mac(0x92)[0] == 0x01:
+ log.debug("mutual authentication completed")
+ self._authenticated = True
+ self.read_from_ndef_service = self.read_with_mac
+ self.write_to_ndef_service = self.write_with_mac
+ else:
+ log.debug("mutual authentication failed")
+
+ return self._authenticated
+
+ def write_with_mac(self, data, block):
+ """Write one data block with additional integrity check.
+
+ If prior to calling this method the tag was not authenticated,
+ a :exc:`RuntimeError` exception is raised.
+
+ Command execution errors raise :exc:`~nfc.tag.TagCommandError`.
+
+ """
+ # Write a single data block protected with a mac. The card
+ # will only accept the write if it computed the same mac.
+ log.debug("write 1 block with mac")
+ if len(data) != 16:
+ raise ValueError("data must be 16 octets")
+ if type(block) is not int:
+ raise ValueError("block number must be int")
+ if self._sk is None or self._iv is None:
+ raise RuntimeError("tag must be authenticated first")
+
+ # The write count is the first three byte of the wcnt block.
+ wcnt = self.read_without_mac(0x90)[0:3]
+ log.debug("write count is %s", hexlify(wcnt[::-1]).decode())
+
+ # We must generate the mac_a block to write the data. The data
+ # to encrypt to the mac is composed of write count and block
+ # numbers (8 byte) and the data we want to write. The mac for
+ # write must be generated with the key flipped (sk2 || sk1).
+ def flip(sk):
+ return sk[8:16] + sk[0:8]
+
+ data = wcnt + b"\x00" + bytearray([block]) + b"\x00\x91\x00" + data
+ maca = self.generate_mac(data, flip(self._sk), self._iv) + wcnt+5*b"\0"
+
+ # Now we can write the data block with our computed mac to the
+ # desired block and the maca block. Write without encryption
+ # means that the data is not encrypted with a service key.
+ sc_list = [tt3.ServiceCode(0, 0b001001)]
+ bc_list = [tt3.BlockCode(block), tt3.BlockCode(0x91)]
+ self.write_without_encryption(sc_list, bc_list, data[8:24] + maca)
+
+
+class FelicaPlug(tt3.Type3Tag):
+ """FeliCa Plug is a contactless communication interface module for
+ microcontrollers.
+
+ """
+ IC_CODE_MAP = {
+ 0xE0: "FeliCa Plug (RC-S926)",
+ 0xE1: "FeliCa Link (RC-S730) Plug Mode",
+ }
+
+ def __init__(self, clf, target):
+ super(FelicaPlug, self).__init__(clf, target)
+ self._product = self.IC_CODE_MAP[self.pmm[1]]
diff --git a/src/lib/nfc/tag/tt4.py b/src/lib/nfc/tag/tt4.py
new file mode 100644
index 0000000..08267eb
--- /dev/null
+++ b/src/lib/nfc/tag/tt4.py
@@ -0,0 +1,579 @@
+# -*- coding: latin-1 -*-
+# -----------------------------------------------------------------------------
+# Copyright 2012, 2017 Stephen Tiedemann
+#
+# Licensed under the EUPL, Version 1.1 or - as soon they
+# will be approved by the European Commission - subsequent
+# versions of the EUPL (the "Licence");
+# You may not use this work except in compliance with the
+# Licence.
+# You may obtain a copy of the Licence at:
+#
+# https://joinup.ec.europa.eu/software/page/eupl
+#
+# Unless required by applicable law or agreed to in
+# writing, software distributed under the Licence is
+# distributed on an "AS IS" basis,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied.
+# See the Licence for the specific language governing
+# permissions and limitations under the Licence.
+# -----------------------------------------------------------------------------
+import itertools
+from binascii import hexlify
+from struct import pack, unpack
+
+import nfc.tag
+import nfc.clf
+
+import logging
+log = logging.getLogger(__name__)
+
+
+ndef_aid_v1 = bytearray.fromhex("D2760000850100")
+ndef_aid_v2 = bytearray.fromhex("D2760000850101")
+
+
+class Type4TagCommandError(nfc.tag.TagCommandError):
+ """Type 4 Tag exception class. Beyond the generic error values from
+ :attr:`~nfc.tag.TagCommandError` this class covers ISO 7816-4
+ response APDU error codes.
+
+ """
+ errno_str = {
+ # ISO/IEC 7816-4 (2005) APDU errors (SW1/SW2)
+ 0x6700: "wrong lenght (general error)",
+ 0x6900: "command not allowed (general error)",
+ 0x6981: "command incompatible with file structure",
+ 0x6982: "security status not satisfied",
+ 0x6A00: "wrong parameters p1/p2 (general error)",
+ 0x6A80: "incorrect parameters in command data field",
+ 0x6A81: "function not supported",
+ 0x6A82: "file or application not found",
+ 0x6A83: "record not found",
+ 0x6A84: "not enough memory space in the file",
+ 0x6A85: "command length inconsistent with TLV structure",
+ 0x6A86: "incorrect parameters p1/p2",
+ 0x6A87: "command length inconsistent with p1/p2",
+ 0x6A88: "referenced data or reference data not found",
+ 0x6A89: "file already exists",
+ 0x6A8A: "file name already exists",
+ }
+
+ @staticmethod
+ def from_status(status):
+ return Type4TagCommandError(unpack(">H", status)[0])
+
+
+class IsoDepInitiator(object):
+ def __init__(self, clf, fsc, fwt):
+ self.clf = clf
+ self.pni = 0
+ self.miu = fsc - 3 # account for 1 byte PCB and 2 byte EDC
+ self.fwt = fwt
+ self.delta_fwt = 49152 / 13.56E6
+ self.n_retry_ack = min(int(1/self.fwt), 5)
+ self.n_retry_nak = self.n_retry_ack
+
+ def exchange(self, command, timeout=None):
+ if timeout is None:
+ timeout = self.fwt + self.delta_fwt
+
+ if command is None:
+ # presence check with R(NAK)
+ data = bytearray([0xB2 | self.pni])
+ self.clf.exchange(data, timeout)
+ return
+
+ for offset in range(0, len(command), self.miu):
+ more = len(command) - offset > self.miu
+ pfb = pack('B', (0x02, 0x12)[more] | self.pni)
+ data = pfb + command[offset:offset+self.miu]
+
+ for i in itertools.count(start=1): # pragma: no branch
+ try:
+ data = self.clf.exchange(data, timeout)
+ if len(data) == 0:
+ raise nfc.clf.TransmissionError
+ if data[0] == 0xA2 | (~self.pni & 1):
+ log.debug("ISO-DEP retransmit after ack")
+ data = pfb + command[offset:offset+self.miu]
+ continue
+ break
+ except nfc.clf.TransmissionError:
+ if i <= self.n_retry_nak:
+ log.warning("ISO-DEP transmission error (#%d)" % i)
+ data = bytearray([0xB2 | self.pni])
+ else:
+ log.error("ISO-DEP unrecoverable transmission error")
+ raise Type4TagCommandError(nfc.tag.RECEIVE_ERROR)
+ except nfc.clf.TimeoutError:
+ if i <= self.n_retry_nak:
+ log.warning("ISO-DEP timeout error (#%d)" % i)
+ data = bytearray([0xB2 | self.pni])
+ else:
+ log.error("ISO-DEP unrecoverable timeout error")
+ raise Type4TagCommandError(nfc.tag.TIMEOUT_ERROR)
+ except nfc.clf.ProtocolError:
+ log.error("ISO-DEP unrecoverable protocol error")
+ raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR)
+
+ while data[0] & 0b11111110 == 0b11110010: # WTX
+ log.debug("ISO-DEP waiting time extension")
+ data = self.clf.exchange(data, (data[1] & 0x3F) * self.fwt)
+
+ if data[0] & 0x01 != self.pni:
+ log.warning("ISO-DEP protocol error: block number")
+ raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR)
+
+ if more:
+ if data[0] & 0b11111110 == 0b10100010: # ACK
+ self.pni = (self.pni + 1) % 2
+ else:
+ log.error("ISO-DEP protocol error: expected ack")
+ raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR)
+ else:
+ if data[0] & 0b11101110 == 0x02: # INF
+ self.pni = (self.pni + 1) % 2
+ response = data[1:]
+ else:
+ log.error("ISO-DEP protocol error: expected inf")
+ raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR)
+
+ while bool(data[0] & 0b00010000):
+ data = pack('B', 0xA2 | self.pni) # ACK
+
+ for i in itertools.count(start=1): # pragma: no branch
+ try:
+ data = self.clf.exchange(data, timeout)
+ if len(data) == 0:
+ raise nfc.clf.TransmissionError
+ break
+ except nfc.clf.TransmissionError:
+ if i <= self.n_retry_ack:
+ log.warning("ISO-DEP transmission error (#%d)" % i)
+ data = bytearray([0xA2 | self.pni])
+ else:
+ log.error("ISO-DEP unrecoverable transmission error")
+ raise Type4TagCommandError(nfc.tag.RECEIVE_ERROR)
+ except nfc.clf.TimeoutError:
+ if i <= self.n_retry_ack:
+ log.warning("ISO-DEP timeout error (#%d)" % i)
+ data = bytearray([0xA2 | self.pni])
+ else:
+ log.error("ISO-DEP unrecoverable timeout error")
+ raise Type4TagCommandError(nfc.tag.TIMEOUT_ERROR)
+ except nfc.clf.ProtocolError:
+ log.error("ISO-DEP unrecoverable protocol error")
+ raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR)
+
+ if data[0] & 0x01 != self.pni:
+ log.error("ISO-DEP protocol error: block number")
+ raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR)
+
+ response = response + data[1:]
+ self.pni = (self.pni + 1) % 2
+
+ return response
+
+
+class Type4Tag(nfc.tag.Tag):
+ """Implementation of the NFC Forum Type 4 Tag operation specification.
+
+ The NFC Forum Type 4 Tag is based on ISO/IEC 14443 DEP protocol
+ for Type A and B modulation and uses ISO/IEC 7816-4 command and
+ response APDUs.
+
+ """
+ TYPE = "Type4Tag"
+
+ class NDEF(nfc.tag.Tag.NDEF):
+ # Type 4 Tag specific implementation of the NDEF access type
+ # class that is returned by the Tag.ndef attribute.
+
+ def _select_ndef_application(self):
+ for self._aid, mrl in ((ndef_aid_v2, 256), (ndef_aid_v1, 0)):
+ try:
+ self.tag.send_apdu(0, 0xA4, 0x04, 0x00, self._aid, mrl)
+ log.debug("selected %s", hexlify(self._aid).decode())
+ return True
+ except Type4TagCommandError as error:
+ if error.errno <= 0:
+ break
+
+ def _select_fid(self, fid):
+ p2 = 0x00 if self._aid == ndef_aid_v1 else 0x0C
+ try:
+ self.tag.send_apdu(0, 0xA4, 0x00, p2, fid)
+ log.debug("selected %s", hexlify(fid).decode())
+ return True
+ except Type4TagCommandError:
+ log.debug("failed to select %s", hexlify(fid).decode())
+
+ def _read_binary(self, offset, size):
+ (p1, p2) = pack(">H", offset)
+ max_data = min(self._max_le, size)
+ log.debug("read_binary from %d to %d", offset, offset + max_data)
+ return self.tag.send_apdu(0, 0xB0, p1, p2, mrl=max_data)
+
+ def _update_binary(self, offset, data):
+ (p1, p2) = pack(">H", offset)
+ max_data = min(self._max_lc, len(data))
+ log.debug("update_binary from %d to %d", offset, offset + max_data)
+ self.tag.send_apdu(0, 0xD6, p1, p2, data[:max_data])
+ return max_data
+
+ def _discover_ndef(self):
+ self._max_lc = 1
+ self._max_le = 15
+
+ log.debug("select ndef application")
+ if not self._select_ndef_application():
+ log.debug("no ndef application file")
+ return False
+
+ log.debug("select ndef capability file")
+ if not self._select_fid(b"\xE1\x03"):
+ log.warning("no ndef capability file")
+ return False
+
+ log.debug("read ndef capability file")
+ cclen = self._read_binary(0, 2)
+ if not (cclen and len(cclen) == 2):
+ log.debug("error reading capability length")
+ return False
+
+ cclen = unpack(">H", cclen)[0]
+ capabilities = self._read_binary(2, min(cclen-2, 15))
+
+ if capabilities is None or len(capabilities) < 13:
+ log.warning("insufficient capability data")
+ return False
+
+ capabilities += (15-len(capabilities)) * b"\0" # for unpack
+ ver, mle, mlc, tag, val = unpack(">BHHB9p", capabilities)
+ log.debug("ndef mapping version %d.%d", ver >> 4, ver & 15)
+ log.debug("max apdu response length %d", mle)
+ log.debug("max apdu command length %d", mlc)
+ log.debug("ndef file control tlv tag %d", tag)
+
+ if ver >> 4 not in (1, 2, 3):
+ log.debug("unsupported major ndef version")
+ return False
+
+ if not (tag, len(val)) in ((4, 6), (6, 8)):
+ log.error("invalid ndef control tlv")
+ return False
+
+ ndef_control_tlv_format = ">2sHBB" if tag == 4 else ">2sIBB"
+ ndef_file, mfs, rf, wf = unpack(ndef_control_tlv_format, val)
+ log.debug("ndef file identifier %s", hexlify(ndef_file).decode())
+ log.debug("ndef file size limit %d", mfs)
+ log.debug("ndef file read flag is %d", rf)
+ log.debug("ndef file write flag is %d", wf)
+
+ self._max_le = mle
+ self._max_lc = mlc
+ self._capacity = mfs - tag + 2
+ self._readable = bool(rf == 0)
+ self._writeable = bool(wf == 0)
+ self._nlen_size = tag - 2
+ self._ndef_file = ndef_file
+
+ return True
+
+ def _read_ndef_data(self):
+ log.debug("read ndef data")
+
+ try:
+ if not (hasattr(self, "_ndef_file") or self._discover_ndef()):
+ log.debug("no ndef application")
+ return None
+
+ log.debug("select ndef data file")
+ if not self._select_fid(self._ndef_file):
+ log.warning("ndef file select error")
+ return None
+
+ log.debug("read ndef data file")
+ lfmt = ">I" if self._nlen_size == 4 else ">H"
+ nlen = self._read_binary(0, self._nlen_size)
+ if len(nlen) != self._nlen_size:
+ return None
+
+ nlen = unpack(lfmt, nlen)[0]
+ log.debug("ndef data length is {0}".format(nlen))
+
+ data = bytearray()
+ while len(data) < nlen:
+ offset = self._nlen_size + len(data)
+ data += self._read_binary(offset, nlen - len(data))
+
+ except Type4TagCommandError:
+ return None
+ else:
+ return data
+
+ def _write_ndef_data(self, data):
+ log.debug("write ndef data")
+
+ lfmt = ">I" if self._nlen_size == 4 else ">H"
+ nlen = bytearray(pack(lfmt, len(data)))
+ if len(nlen) + len(data) <= self._max_lc:
+ data = bytearray(nlen) + data
+ nlen = None
+ else:
+ data = bytearray(len(nlen)) + data
+
+ offset = 0
+ while offset < len(data):
+ offset += self._update_binary(offset, data[offset:])
+
+ if nlen:
+ self._update_binary(0, nlen)
+
+ return True
+
+ def _wipe_ndef_data(self, wipe=None):
+ lfmt = ">I" if self._nlen_size == 4 else ">H"
+ nlen = bytearray(pack(lfmt, 0))
+ self._update_binary(0, nlen)
+ offset = self._nlen_size
+ data = bytearray(self._capacity * [wipe % 256])
+ while offset < self.capacity:
+ offset += self._update_binary(offset, data[offset:])
+
+ def _dump_ndef_data(self):
+ lines = []
+ for offset in itertools.count(0, 16): # pragma: no branch
+ try:
+ line = self._read_binary(offset, 16)
+ if len(line) > 0:
+ lines.append(line)
+ if len(line) < 16:
+ break
+ except Type4TagCommandError:
+ break
+
+ return lines
+
+ def _is_present(self):
+ try:
+ self._dep.exchange(None)
+ return True
+ except nfc.clf.CommunicationError:
+ return False
+
+ def dump(self):
+ """Returns tag data as a list of formatted strings.
+
+ The :meth:`dump` method provides useful output only for NDEF
+ formatted Type 4 Tags. Each line that is returned contains a
+ hexdump of 16 octets from the NDEF data file.
+
+ """
+ return self._dump()
+
+ def _dump(self):
+ def oprint(octets):
+ return ' '.join(['%02x' % x for x in octets])
+
+ def cprint(octets):
+ return ''.join([chr(x) if 32 <= x <= 126 else '.' for x in octets])
+
+ def lprint(fmt, octets, index):
+ return fmt.format(index, oprint(octets), cprint(octets))
+
+ lfmt = "0x{0:04x}: {1} |{2}|"
+
+ if self.ndef and self.ndef.is_readable:
+ lines = self.ndef._dump_ndef_data()
+ return [lprint(lfmt, d, i << 4) for i, d in enumerate(lines)]
+
+ return []
+
+ def format(self, version=None, wipe=None):
+ """Erase the NDEF message on a Type 4 Tag.
+
+ The :meth:`format` method writes the length of the NDEF
+ message on a Type 4 Tag to zero, thus the tag will appear to
+ be empty. If the *wipe* argument is set to some integer then
+ :meth:`format` will also overwrite all user data with that
+ integer (mod 256).
+
+ Despite it's name, the :meth:`format` method can not format a
+ blank tag to make it NDEF compatible; this requires
+ proprietary information from the manufacturer.
+
+ """
+ return super(Type4Tag, self).format(version, wipe)
+
+ def _format(self, version, wipe):
+ if not self.ndef or not self.ndef.is_writeable:
+ log.error("format error: no ndef or not writeable")
+ return False
+
+ if wipe is not None:
+ try:
+ self.ndef._wipe_ndef_data(wipe)
+ except Type4TagCommandError as error:
+ log.error("format error: %s", str(error))
+ return False
+
+ return True
+
+ def transceive(self, data, timeout=None):
+ """Transmit arbitrary data and receive the response.
+
+ This is a low level method to send arbitrary data to the
+ tag. While it should almost always be better to use
+ :meth:`send_apdu` this is the only way to force a specific
+ timeout value (which is otherwise derived from the Tag's
+ answer to select). The *timeout* value is expected as a float
+ specifying the seconds to wait.
+
+ """
+ log.debug(">> {0}".format(hexlify(data).decode()))
+ data = self._dep.exchange(data, timeout)
+ log.debug("<< {0}".format(hexlify(data).decode() if data else "None"))
+ return data
+
+ def send_apdu(self, cla, ins, p1, p2, data=None, mrl=0, check_status=True):
+ """Send an ISO/IEC 7816-4 APDU to the Type 4 Tag.
+
+ The 4 byte APDU header (class, instruction, parameter 1 and 2)
+ is constructed from the first four parameters (cla, ins, p1,
+ p2) without interpretation. The byte string *data* argument
+ represents the APDU command data field. It is encoded as a
+ short or extended length field followed by the *data*
+ bytes. The length field is not transmitted if *data* is None
+ or an empty string. The maximum acceptable number of response
+ data bytes is given with the max-response-length *mrl*
+ argument. The value of *mrl* is transmitted as the 7816-4 APDU
+ Le field after appropriate conversion.
+
+ By default, the response is returned as a byte array not
+ including the status word, a :exc:`Type4TagCommandError`
+ exception is raised for any status word other than
+ 9000h. Response status verification can be disabled with
+ *check_status* set to False, the byte array will then include
+ the response status word at the last two positions.
+
+ Transmission errors always raise a :exc:`Type4TagCommandError`
+ exception.
+
+ """
+ apdu = bytearray([cla, ins, p1, p2])
+
+ if not self._extended_length_support:
+ if data and len(data) > 255:
+ raise ValueError("unsupported command data length")
+ if mrl and mrl > 256:
+ raise ValueError("unsupported max response length")
+ if data:
+ apdu += pack('>B', len(data)) + bytes(data)
+ if mrl > 0:
+ apdu += pack('>B', 0 if mrl == 256 else mrl)
+ else:
+ if data and len(data) > 65535:
+ raise ValueError("invalid command data length")
+ if mrl and mrl > 65536:
+ raise ValueError("invalid max response length")
+ if data:
+ apdu += pack(">xH", len(data)) + bytes(data)
+ if mrl > 0:
+ le = 0 if mrl == 65536 else mrl
+ apdu += pack(">H", le) if data else pack(">xH", le)
+
+ apdu = self.transceive(apdu)
+
+ if not apdu or len(apdu) < 2:
+ raise Type4TagCommandError(nfc.tag.PROTOCOL_ERROR)
+
+ if check_status and apdu[-2:] != b"\x90\x00":
+ raise Type4TagCommandError.from_status(apdu[-2:])
+
+ return apdu[:-2] if check_status else apdu
+
+ def __str__(self):
+ s = "{tag.__class__.__name__} MIU={tag._dep.miu} FWT={tag._dep.fwt:f}"
+ return s.format(tag=self)
+
+
+class Type4ATag(Type4Tag):
+ def __init__(self, clf, target):
+ super(Type4ATag, self).__init__(clf, target)
+ self._nfcid = bytearray(target.sdd_res)
+
+ log.debug("send RATS command to activate the Type 4A Tag")
+ if self.clf.max_recv_data_size < 256:
+ log.warning("{0} does not support fsd 256".format(self.clf))
+ rats_cmd = bytearray.fromhex("E0 70")
+ else:
+ rats_cmd = bytearray.fromhex("E0 80")
+ rats_res = self.clf.exchange(rats_cmd, timeout=0.03)
+ log.debug("rcvd RATS response: {0}".format(hexlify(rats_res).decode()))
+
+ fsci, fwti = rats_res[1] & 0x0F, rats_res[3] >> 4
+ if fsci > 8:
+ log.warning("FSCI with RFU value in RATS_RES")
+ fsci = 8
+ if fwti > 14:
+ log.warning("FWI with RFU value in RATS_RES")
+ fwti = 4
+
+ fsc = (16, 24, 32, 40, 48, 64, 96, 128, 256)[fsci]
+ fwt = 4096 / 13.56E6 * (2**fwti)
+
+ if fsc > self.clf.max_send_data_size:
+ log.warning("{0} does not support fsc {1}".format(self.clf, fsc))
+ fsc = self.clf.max_send_data_size
+
+ log.debug("max command frame size is {0:d} byte".format(fsc))
+ log.debug("max frame waiting time is {0:f}".format(fwt))
+
+ self._dep = IsoDepInitiator(clf, fsc, fwt)
+ self._extended_length_support = False
+
+
+class Type4BTag(Type4Tag):
+ def __init__(self, clf, target):
+ super(Type4BTag, self).__init__(clf, target)
+ self._nfcid = bytearray(target.sensb_res[1:5])
+
+ log.debug("send ATTRIB command to activate the Type 4B Tag")
+ if self.clf.max_recv_data_size < 256:
+ log.warning("{0} does not support fsd 256".format(self.clf))
+ attrib_cmd = b'\x1D' + self._nfcid + b'\x00\x07\x01\x00'
+ else:
+ attrib_cmd = b'\x1D' + self._nfcid + b'\x00\x08\x01\x00'
+ attrib_res = self.clf.exchange(attrib_cmd, timeout=0.03)
+ log.debug("rcvd ATTRIB response %s", hexlify(attrib_res).decode())
+
+ fsci, fwti = target.sensb_res[10] >> 4, target.sensb_res[11] >> 4
+ if fsci > 8:
+ log.warning("FSCI with RFU value in SENSB_RES")
+ fsci = 8
+ if fwti > 14:
+ log.warning("FWI with RFU value in SENSB_RES")
+ fwti = 4
+
+ fsc = (16, 24, 32, 40, 48, 64, 96, 128, 256)[fsci]
+ fwt = 4096 / 13.56E6 * (2**fwti)
+
+ if fsc > self.clf.max_send_data_size:
+ log.warning("{0} does not support fsc {1}".format(self.clf, fsc))
+ fsc = self.clf.max_send_data_size
+
+ log.debug("max command frame size is {0:d} byte".format(fsc))
+ log.debug("max frame waiting time is {0:f}".format(fwt))
+
+ self._dep = IsoDepInitiator(clf, fsc, fwt)
+ self._extended_length_support = False
+
+
+def activate(clf, target):
+ if target.brty.endswith('A'):
+ return Type4ATag(clf, target)
+ if target.brty.endswith('B'):
+ return Type4BTag(clf, target)
diff --git a/src/test/rfid.py b/src/test/rfid.py
index 14caac2..8a2fa16 100644
--- a/src/test/rfid.py
+++ b/src/test/rfid.py
@@ -8,8 +8,8 @@ from pathlib import Path
import serial
import ndef
-import nfc
-from nfc.clf import RemoteTarget
+from src.lib import nfc as nfc
+from src.lib.nfc.clf import RemoteTarget
logging.basicConfig(
format="{asctime}:{name}:{levelname}:{message}",
diff --git a/src/ui/barcode_recipe_selection/barcode_recipe_selection.py b/src/ui/barcode_recipe_selection/barcode_recipe_selection.py
index a1f7061..8de4123 100644
--- a/src/ui/barcode_recipe_selection/barcode_recipe_selection.py
+++ b/src/ui/barcode_recipe_selection/barcode_recipe_selection.py
@@ -75,7 +75,7 @@ class Barcode_Recipe_Selection(Test_Test):
else:
lines = data.splitlines()
#lines = data.split("-")
- candidates = [i for i in lines if len(i) in(10,12)]
+ candidates = [i for i in lines if len(i)in (9,10,12)]
if len(candidates)>0:
# RECIPE CODE FOUND
self.recipe=candidates[-1]
diff --git a/src/ui/recipe_selection/recipe_selection.py b/src/ui/recipe_selection/recipe_selection.py
index 33f60ad..b5c4a26 100755
--- a/src/ui/recipe_selection/recipe_selection.py
+++ b/src/ui/recipe_selection/recipe_selection.py
@@ -10,6 +10,8 @@ from PyQt5.QtCore import QTimer, pyqtSignal
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QShortcut
import shutil
+
+from lib.helpers.recipe_manager import export_recipes, import_recipes
from ui.crud import Crud, Json_External_Dialog_Editor_Cell_Widget
from ui.helpers import replace_widget
from ui.recipe_spec_and_step_editor import Recipe_Spec_And_Step_Editor
@@ -275,227 +277,22 @@ class Recipe_Selection(Widget):
# IMPORT RECIPES FROM CSV FILE TO DATABASE
def import_recipes(self, csv_path=None, defaults=None):
- if defaults is None:
- global noner
- defaults = self.config.get("recipes_defaults", noner)
- if csv_path is None:
- options = QFileDialog.Options()
- options |= QFileDialog.DontUseNativeDialog
- csv_path, _ = QFileDialog.getOpenFileName(
- self,
- "Importazione ricette",
- "ricette.csv",
- "CSV data (*.csv);;All Files (*)",
- options=options,
- )
- csv_path = str(csv_path)
- if not len(csv_path):
- return
- self.log.info(f"recipes: importing recipes from {csv_path}")
- recipe_name_field = self.config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip()
- part_number_field = self.config.get("recipe", {}).get("part_number_field", "part_number").strip()
- description_field = self.config.get("recipe", {}).get("description_field", "descrizione").strip()
- barcode_enable_field = self.config.get("recipe", {}).get("barcode_enable_field", "verifica_codice_a_barre_abilitata").strip()
-
- with open(csv_path, "r", encoding="utf-8-sig") as f:
- reader = csv.DictReader(f)
- count = 0
- for ucrow in reader:
- row = dict((k.lower(), v) for k, v in ucrow.items())
- recipe_name = row.get(recipe_name_field, defaults["codice_ricetta"])
- steps_specs = self.read_steps(row, defaults=defaults)
-
- # create recipe or update existing one in DB
- try:
- recipe = Recipes.get_by_id(recipe_name)
- steps = recipe.get_steps_map()
- recipe_is_new = False
- except Recipes.DoesNotExist:
- recipe = Recipes(name=recipe_name, part_number="TEMPORARY")
- steps = {}
- for step_name, step_spec in steps_specs.items():
- if step_name not in self.unsupported_steps:
- steps[step_name] = step_spec
- recipe_is_new = True
- recipe.client = row.get("cliente", defaults["cliente"])
- recipe.part_number = row.get(part_number_field, defaults["part_number"])
- recipe.description = row.get(description_field, defaults["descrizione"])
- recipe.spec = {
- "count": len(row.get("dimensione_lotto_abilitata", defaults["dimensione_lotto_abilitata"])) and "count" not in self.unsupported_steps,
- "connector": len(
- row.get("verifica_connettore_abilitata", defaults["verifica_connettore_abilitata"])) and "connector" not in self.unsupported_steps,
- "barcodes": len(row.get(barcode_enable_field, defaults["verifica_codice_a_barre_abilitata"])) and "barcodes" not in self.unsupported_steps,
- "resistance": len(row.get("verifica_resistenza_connettore_abilitata",
- defaults["verifica_resistenza_connettore_abilitata"])) and "resistance" not in self.unsupported_steps,
- "screws": len(row.get("avvitatura_abilitata", defaults["avvitatura_abilitata"])) and "screws" not in self.unsupported_steps,
- "instruction": len(row.get("istruzione_abilitata", defaults["istruzione_abilitata"])) and "instruction" not in self.unsupported_steps,
- "instruction_extra": len(row.get("istruzione_abilitata_extra", defaults["istruzione_abilitata_extra"])) and "instruction_extra" not in self.unsupported_steps,
- "leak_1": len(row.get("prova_tenuta_abilitata", defaults["prova_tenuta_abilitata"])) and "leak_1" not in self.unsupported_steps,
- "leak_2": len(row.get("prova_tenuta_abilitata_2", defaults["prova_tenuta_abilitata_2"])) and "leak_2" not in self.unsupported_steps,
- "vision": len(row.get("test_visione_abilitato", defaults["test_visione_abilitato"])) and "vision" not in self.unsupported_steps,
- "print": len(row.get("stampa_etichetta_abilitata", defaults["stampa_etichetta_abilitata"])) and "print" not in self.unsupported_steps,
- "steps": steps,
- }
- recipe.spec["steps"]=steps_specs
- if recipe_is_new:
- recipe.save(force_insert=True)
- else:
- recipe.save()
- count += 1
- db.commit()
- self.log.info(f"recipes: imported {count} rows.")
- self.crud.refresh()
+ import_recipes(
+ config=self.config,
+ csv_path=csv_path,
+ defaults=defaults,
+ unsupported_steps=self.unsupported_steps,
+ logger=self.log,
+ )
# EXPORT RECIPES TABLE TO CSV FILE
def export_recipes(self, csv_path=None):
- if csv_path is None:
- options = QFileDialog.Options()
- options |= QFileDialog.DontUseNativeDialog
- csv_path, _ = QFileDialog.getSaveFileName(
- self,
- "Esportazione ricette",
- "ricette.csv",
- "CSV data (*.csv);;All Files (*)",
- options=options,
- )
- csv_path = str(csv_path)
- if not len(csv_path):
- return
- if not csv_path.lower().endswith(".csv"):
- csv_path += ".csv"
- csv_dir = os.path.dirname(csv_path)
- if len(csv_dir):
- os.makedirs(csv_dir, exist_ok=True)
- recipe_name_field = self.config.get("recipe", {}).get("recipe_name_field", "codice_ricetta").strip()
- barcode_enable_field = self.config.get("recipe", {}).get("barcode_enable_field", "verifica_codice_a_barre_abilitata").strip()
- barcode_serial_field = self.config.get("recipe", {}).get("barcode_serial_field", "codice_a_barre").strip()
- print_template_field = self.config.get("recipe", {}).get("label_template_field", "modello_etichetta").strip()
- data = []
- fieldnames = set() # Use a set to avoid duplicates
- for recipe in list(Recipes.select()):
- steps = recipe.get_steps_map()
- exportable = {
- # BASE SECTION
- recipe_name_field: recipe.name,
- "cliente": recipe.client,
- "part_number": recipe.part_number,
- }
+ export_recipes(
+ config=self.config,
+ csv_path=csv_path,
+ logger=self.log,
+ )
- # Add base fields to the fieldnames
- fieldnames.update([recipe_name_field, "cliente", "part_number"])
-
- # Check and add fields conditionally for each section
- if "connector" in steps:
- exportable.update({
- "verifica_connettore_abilitata": "x",
- "connettore": steps["connector"].spec["connector"]
- })
- fieldnames.update(["verifica_connettore_abilitata", "connettore"])
-
- if "resistance" in steps:
- exportable.update({
- "verifica_resistenza_connettore_abilitata": "x",
- "scala_resistenza": steps["resistance"].spec["scale"],
- "r nominale": steps["resistance"].spec["expected"],
- "tolleranza_resistenza_pos": steps["resistance"].spec["tolerance_pos"],
- "tolleranza_resistenza_neg": steps["resistance"].spec["tolerance_neg"]
- })
- fieldnames.update(["verifica_resistenza_connettore_abilitata", "scala_resistenza", "r nominale",
- "tolleranza_resistenza_pos", "tolleranza_resistenza_neg"])
-
- if "barcodes" in steps:
- exportable.update({
- barcode_enable_field: "x",
- barcode_serial_field: steps["barcodes"].spec["serial"]
- })
- fieldnames.update([barcode_enable_field, barcode_serial_field])
-
- if recipe.spec.get("steps", {}).get("screws") and "screws" in steps:
- exportable.update({
- "avvitatura_abilitata": "x",
- "viti": steps["screws"].spec["quantity"]
- })
- fieldnames.update(["avvitatura_abilitata", "viti"])
-
- if "leak_1" in steps:
- exportable.update({
- "prova_tenuta_abilitata": "x",
- "tempo_pre_riempimento": steps["leak_1"].spec["pre_filling_time"],
- "pressione_pre_riempimento": steps["leak_1"].spec["pre_filling_pressure"],
- "tempo_riempimento": steps["leak_1"].spec["filling_time"],
- "tempo_assestamento": steps["leak_1"].spec["settling_time"],
- "percentuale_minima_pressione_assestamento": steps["leak_1"].spec["settling_pressure_min_percent"],
- "percentuale_massima_pressione_assestamento": steps["leak_1"].spec["settling_pressure_max_percent"],
- "tempo_di_test": steps["leak_1"].spec["test_time"],
- "pressione_di_test_delta_minimo": steps["leak_1"].spec["test_pressure_qneg"],
- "pressione_di_test": steps["leak_1"].spec["test_pressure"],
- "pressione_di_test_delta_massimo": steps["leak_1"].spec["test_pressure_qpos"],
- "tempo_svuotamento": steps["leak_1"].spec["flush_time"],
- "pressione_svuotamento": steps["leak_1"].spec["flush_pressure"],
- })
- fieldnames.update(["prova_tenuta_abilitata", "tempo_pre_riempimento", "pressione_pre_riempimento",
- "tempo_riempimento", "tempo_assestamento",
- "percentuale_minima_pressione_assestamento",
- "percentuale_massima_pressione_assestamento", "tempo_di_test",
- "pressione_di_test_delta_minimo",
- "pressione_di_test", "pressione_di_test_delta_massimo", "tempo_svuotamento",
- "pressione_svuotamento"])
-
- if "leak_2" in steps:
- exportable.update({
- "prova_tenuta_abilitata_2": "x",
- "tempo_pre_riempimento_2": steps["leak_2"].spec["pre_filling_time"],
- "pressione_pre_riempimento_2": steps["leak_2"].spec["pre_filling_pressure"],
- "tempo_riempimento_2": steps["leak_2"].spec["filling_time"],
- "tempo_assestamento_2": steps["leak_2"].spec["settling_time"],
- "percentuale_minima_pressione_assestamento_2": steps["leak_2"].spec[
- "settling_pressure_min_percent"],
- "percentuale_massima_pressione_assestamento_2": steps["leak_2"].spec[
- "settling_pressure_max_percent"],
- "tempo_di_test_2": steps["leak_2"].spec["test_time"],
- "pressione_di_test_delta_minimo_2": steps["leak_2"].spec["test_pressure_qneg"],
- "pressione_di_test_2": steps["leak_2"].spec["test_pressure"],
- "pressione_di_test_delta_massimo_2": steps["leak_2"].spec["test_pressure_qpos"],
- "tempo_svuotamento_2": steps["leak_2"].spec["flush_time"],
- "pressione_svuotamento_2": steps["leak_2"].spec["flush_pressure"],
- })
- fieldnames.update(["prova_tenuta_abilitata_2", "tempo_pre_riempimento_2", "pressione_pre_riempimento_2",
- "tempo_riempimento_2", "tempo_assestamento_2",
- "percentuale_minima_pressione_assestamento_2",
- "percentuale_massima_pressione_assestamento_2", "tempo_di_test_2",
- "pressione_di_test_delta_minimo_2", "pressione_di_test_2",
- "pressione_di_test_delta_massimo_2",
- "tempo_svuotamento_2", "pressione_svuotamento_2"])
-
- if "vision" in steps:
- exportable.update({
- "test_visione_abilitato": recipe.spec["vision"],
- "ricetta_visione": steps["vision"].spec["recipe"]
- })
- fieldnames.update(["test_visione_abilitato", "ricetta_visione"])
-
- if "print" in steps:
- exportable.update({
- "stampa_etichetta_abilitata": "x",
- print_template_field: steps["print"].spec["template"],
- "etichette_supplementari": steps["print"].spec["extra_label"]
- })
- fieldnames.update(["stampa_etichetta_abilitata", print_template_field, "etichette_supplementari"])
-
- # Append the exportable dictionary to the data list
- data.append(exportable)
-
- # Convert the set to a list for CSV writing
- fieldnames = list(fieldnames)
-
- # Export to CSV if there is data
- if len(data):
- self.log.info(f"recipes: exporting recipes to {csv_path}")
- with open(csv_path, "w", newline="") as f:
- w = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
- w.writeheader()
- w.writerows(data)
- self.log.info(f"recipes: exported {len(data)} rows.")
def delete_recipes(self):
ret = QMessageBox.warning(
None,
@@ -507,40 +304,6 @@ class Recipe_Selection(Widget):
if ret == QMessageBox.Ok:
Recipes.delete().execute()
self.crud.refresh()
- def backup_current_recipes(self):
- # Define the backup directory and file name
- backup_dir = os.path.join('config', 'csv_import', 'backup_csv')
- timestamp = datetime.now().strftime("%d%m%y%H%M%S")
- backup_file = f"backup_{timestamp}.csv"
- backup_path = os.path.join(backup_dir, backup_file)
- # Ensure the backup directory exists
- os.makedirs(backup_dir, exist_ok=True)
- # Export current recipes to backup file
- self.export_recipes(csv_path=backup_path)
- def move_imported_csv(self, csv_path):
- # Move the imported CSV to the 'imported_csv' directory
- imported_dir = os.path.join('config', 'csv_import', 'imported_csv')
- os.makedirs(imported_dir, exist_ok=True)
- imported_path = os.path.join(imported_dir, os.path.basename(csv_path))
- shutil.move(csv_path, imported_path)
- self.log.info(f"Imported CSV moved to {imported_path}")
- return imported_path
-
- def check_and_import_auto_csv(self):
- # Define the directory to check
- auto_import_dir = os.path.join('config', 'csv_import', 'auto_csv_import')
-
- # Check if the directory exists and is not empty
- if os.path.exists(auto_import_dir) and os.listdir(auto_import_dir):
- # Perform backup
- self.backup_current_recipes()
-
- # Move and import each CSV file in the directory
- for csv_file in os.listdir(auto_import_dir):
- csv_path = os.path.join(auto_import_dir, csv_file)
- if os.path.isfile(csv_path) and csv_file.endswith(".csv"):
- self.import_recipes(csv_path=csv_path)
- self.move_imported_csv(csv_path)